summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitlab-ci.yml250
-rw-r--r--CHANGELOG.rst33
-rw-r--r--CMakeLists.txt57
-rw-r--r--README.rst13
-rw-r--r--cmake/Modules/CodeCoverage.cmake2
-rw-r--r--conf/biboumi.cfg20
l---------[-rw-r--r--]conf/irc.gnome.org.policy.txt2
-rw-r--r--doc/Makefile20
-rw-r--r--doc/admin.rst305
-rw-r--r--doc/biboumi.1.rst762
-rw-r--r--doc/conf.py146
-rw-r--r--doc/contributing.rst (renamed from CONTRIBUTING.rst)37
-rw-r--r--doc/developer.rst302
-rw-r--r--doc/example.conf14
-rw-r--r--doc/index.rst24
-rw-r--r--doc/install.rst (renamed from INSTALL.rst)124
-rw-r--r--doc/man_index.rst9
-rw-r--r--doc/synopsis.rst4
-rw-r--r--doc/user.rst513
-rw-r--r--docker/biboumi-test/alpine/Dockerfile4
-rw-r--r--docker/biboumi-test/debian/Dockerfile8
-rw-r--r--docker/biboumi-test/fedora/Dockerfile6
-rw-r--r--docker/biboumi/alpine/Dockerfile72
-rw-r--r--images/biboumi.svg31
-rw-r--r--packaging/biboumi.spec.cmake20
-rw-r--r--src/bridge/bridge.cpp112
-rw-r--r--src/bridge/bridge.hpp13
-rw-r--r--src/bridge/history_limit.hpp2
-rw-r--r--src/database/count_query.hpp4
-rw-r--r--src/database/database.cpp5
-rw-r--r--src/database/database.hpp9
-rw-r--r--src/database/postgresql_statement.hpp10
-rw-r--r--src/database/query.cpp5
-rw-r--r--src/database/query.hpp9
-rw-r--r--src/database/row.hpp2
-rw-r--r--src/database/select_query.hpp2
-rw-r--r--src/irc/irc_client.cpp105
-rw-r--r--src/irc/irc_client.hpp26
-rw-r--r--src/irc/irc_message.hpp4
-rw-r--r--src/main.cpp98
-rw-r--r--src/network/credentials_manager.cpp3
-rw-r--r--src/network/credentials_manager.hpp3
-rw-r--r--src/network/resolver.cpp6
-rw-r--r--src/network/tcp_client_socket_handler.cpp9
-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.hpp4
-rw-r--r--src/utils/encoding.cpp26
-rw-r--r--src/utils/get_first_non_empty.cpp5
-rw-r--r--src/utils/get_first_non_empty.hpp7
-rw-r--r--src/utils/optional_bool.hpp2
-rw-r--r--src/utils/string.cpp4
-rw-r--r--src/utils/time.cpp8
-rw-r--r--src/utils/tokens_bucket.hpp60
-rw-r--r--src/xmpp/adhoc_commands_handler.cpp5
-rw-r--r--src/xmpp/biboumi_adhoc_commands.cpp104
-rw-r--r--src/xmpp/biboumi_component.cpp63
-rw-r--r--src/xmpp/biboumi_component.hpp3
-rw-r--r--src/xmpp/jid.cpp2
-rw-r--r--src/xmpp/xmpp_component.cpp16
-rw-r--r--src/xmpp/xmpp_component.hpp1
-rw-r--r--src/xmpp/xmpp_parser.cpp2
-rw-r--r--tests/end_to_end/__main__.py3086
-rw-r--r--tests/end_to_end/functions.py169
-rw-r--r--tests/end_to_end/scenarios/__init__.py10
-rw-r--r--tests/end_to_end/scenarios/basic_handshake_success.py8
-rw-r--r--tests/end_to_end/scenarios/basic_subscribe_unsubscribe.py23
-rw-r--r--tests/end_to_end/scenarios/channel_custom_topic.py30
-rw-r--r--tests/end_to_end/scenarios/channel_force_join.py45
-rw-r--r--tests/end_to_end/scenarios/channel_history.py18
-rw-r--r--tests/end_to_end/scenarios/channel_history_on_fixed_server.py20
-rw-r--r--tests/end_to_end/scenarios/channel_join_on_fixed_irc_server.py13
-rw-r--r--tests/end_to_end/scenarios/channel_join_with_different_nick.py14
-rw-r--r--tests/end_to_end/scenarios/channel_join_with_password.py35
-rw-r--r--tests/end_to_end/scenarios/channel_join_with_two_users.py25
-rw-r--r--tests/end_to_end/scenarios/channel_list_escaping.py10
-rw-r--r--tests/end_to_end/scenarios/channel_list_with_rsm.py67
-rw-r--r--tests/end_to_end/scenarios/channel_messages.py70
-rw-r--r--tests/end_to_end/scenarios/client_error.py16
-rw-r--r--tests/end_to_end/scenarios/complete_channel_list_with_pages_of_3.py106
-rw-r--r--tests/end_to_end/scenarios/configure_bad_value.py21
-rw-r--r--tests/end_to_end/scenarios/default_channel_list_limit.py52
-rw-r--r--tests/end_to_end/scenarios/default_mam_limit.py105
-rw-r--r--tests/end_to_end/scenarios/encoded_channel_join.py10
-rw-r--r--tests/end_to_end/scenarios/execute_admin_disconnect_from_server_adhoc_command.py68
-rw-r--r--tests/end_to_end/scenarios/execute_disconnect_user_adhoc_command.py19
-rw-r--r--tests/end_to_end/scenarios/execute_forbidden_adhoc_command.py7
-rw-r--r--tests/end_to_end/scenarios/execute_hello_adhoc_command.py14
-rw-r--r--tests/end_to_end/scenarios/execute_incomplete_hello_adhoc_command.py11
-rw-r--r--tests/end_to_end/scenarios/execute_ping_adhoc_command.py6
-rw-r--r--tests/end_to_end/scenarios/execute_reload_adhoc_command.py6
-rw-r--r--tests/end_to_end/scenarios/fixed_irc_server_subscription.py8
-rw-r--r--tests/end_to_end/scenarios/fixed_muc_disco_info.py14
-rw-r--r--tests/end_to_end/scenarios/get_irc_connection_info.py15
-rw-r--r--tests/end_to_end/scenarios/get_irc_connection_info_fixed.py17
-rw-r--r--tests/end_to_end/scenarios/global_configure.py27
-rw-r--r--tests/end_to_end/scenarios/global_configure_fixed.py32
-rw-r--r--tests/end_to_end/scenarios/global_configure_persistent_by_default.py15
-rw-r--r--tests/end_to_end/scenarios/invite_other.py21
-rw-r--r--tests/end_to_end/scenarios/irc_channel_configure.py35
-rw-r--r--tests/end_to_end/scenarios/irc_channel_configure_fixed.py33
-rw-r--r--tests/end_to_end/scenarios/irc_channel_configure_xep0045.py20
-rw-r--r--tests/end_to_end/scenarios/irc_server_configure.py105
-rw-r--r--tests/end_to_end/scenarios/irc_server_connection.py7
-rw-r--r--tests/end_to_end/scenarios/irc_server_connection_failure.py11
-rw-r--r--tests/end_to_end/scenarios/irc_server_presence_in_roster.py25
-rw-r--r--tests/end_to_end/scenarios/irc_server_presence_subscription.py6
-rw-r--r--tests/end_to_end/scenarios/irc_tls_connection.py25
-rw-r--r--tests/end_to_end/scenarios/join_history_limit.py108
-rw-r--r--tests/end_to_end/scenarios/leave_unjoined_chan.py16
-rw-r--r--tests/end_to_end/scenarios/list_adhoc.py10
-rw-r--r--tests/end_to_end/scenarios/list_adhoc_fixed_server.py12
-rw-r--r--tests/end_to_end/scenarios/list_adhoc_irc.py8
-rw-r--r--tests/end_to_end/scenarios/list_admin_adhoc.py9
-rw-r--r--tests/end_to_end/scenarios/list_admin_adhoc_fixed_server.py12
-rw-r--r--tests/end_to_end/scenarios/list_muc_user_adhoc.py6
-rw-r--r--tests/end_to_end/scenarios/mam_on_fixed_server.py21
-rw-r--r--tests/end_to_end/scenarios/mam_with_timestamps.py63
-rw-r--r--tests/end_to_end/scenarios/mode_change.py52
-rw-r--r--tests/end_to_end/scenarios/muc_disco_info.py28
-rw-r--r--tests/end_to_end/scenarios/muc_message_from_unjoined_resource.py16
-rw-r--r--tests/end_to_end/scenarios/muc_traffic_info.py6
-rw-r--r--tests/end_to_end/scenarios/multiline_message.py62
-rw-r--r--tests/end_to_end/scenarios/multiline_topic.py11
-rw-r--r--tests/end_to_end/scenarios/multiple_channels_join.py18
-rw-r--r--tests/end_to_end/scenarios/multisession_kick.py46
-rw-r--r--tests/end_to_end/scenarios/multisessionnick.py125
-rw-r--r--tests/end_to_end/scenarios/nick_change_in_join.py18
-rw-r--r--tests/end_to_end/scenarios/not_connected_error.py12
-rw-r--r--tests/end_to_end/scenarios/notices.py10
-rw-r--r--tests/end_to_end/scenarios/persistent_channel.py48
-rw-r--r--tests/end_to_end/scenarios/quit.py12
-rw-r--r--tests/end_to_end/scenarios/raw_message.py12
-rw-r--r--tests/end_to_end/scenarios/raw_message_fixed_irc_server.py15
-rw-r--r--tests/end_to_end/scenarios/raw_names_command.py13
-rw-r--r--tests/end_to_end/scenarios/resource_is_removed_from_server_when_last_chan_is_left.py42
-rw-r--r--tests/end_to_end/scenarios/self_disco_info.py11
-rw-r--r--tests/end_to_end/scenarios/self_invite.py7
-rw-r--r--tests/end_to_end/scenarios/self_ping_fixed_server.py11
-rw-r--r--tests/end_to_end/scenarios/self_ping_not_in_muc.py15
-rw-r--r--tests/end_to_end/scenarios/self_ping_on_real_channel.py23
-rw-r--r--tests/end_to_end/scenarios/self_ping_with_error.py13
-rw-r--r--tests/end_to_end/scenarios/self_version.py39
-rw-r--r--tests/end_to_end/scenarios/simple_channel_join.py21
-rw-r--r--tests/end_to_end/scenarios/simple_channel_join_fixed.py12
-rw-r--r--tests/end_to_end/scenarios/simple_channel_list.py14
-rw-r--r--tests/end_to_end/scenarios/simple_kick.py49
-rw-r--r--tests/end_to_end/scenarios/simple_mam.py60
-rw-r--r--tests/end_to_end/scenarios/slash_me_channel_message.py18
-rw-r--r--tests/end_to_end/sequences.py108
-rw-r--r--tests/jid.cpp2
153 files changed, 4948 insertions, 4309 deletions
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 8807fbd..73bc720 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:
@@ -17,12 +18,22 @@ variables:
LIBIDN: "-DWITH_LIBIDN=1"
SQLITE3: "-DWITH_SQLITE3=1"
POSTGRESQL: "-WITH_POSTGRESQL=1"
+ CXX_FLAGS: "-Werror -Wno-psabi"
#
## Build jobs
#
-.template:basic_build: &basic_build
+.sources_changed:
+ only:
+ changes:
+ - "CMakeLists.txt"
+ - ".gitlab-ci.yml"
+ - "src/**/*"
+ - "tests/**/*"
+
+.basic_build:
+ extends: .sources_changed
stage: build
tags:
- docker
@@ -30,170 +41,156 @@ variables:
- "echo Running cmake with the following parameters: -DCMAKE_CXX_COMPILER=${COMPILER} -DCMAKE_BUILD_TYPE=${BUILD_TYPE} ${BOTAN} ${UDNS} ${SYSTEMD} ${LIBIDN} ${SQLITE3} ${POSTGRESQL}"
- mkdir build/
- cd build/
- - cmake .. -DCMAKE_CXX_FLAGS="-Werror -Wno-psabi" -DCMAKE_CXX_COMPILER=${COMPILER} -DCMAKE_BUILD_TYPE=${BUILD_TYPE} ${BOTAN} ${UDNS} ${SYSTEMD} ${LIBIDN} ${SQLITE3} ${POSTGRESQL}
+ - cmake .. -DCMAKE_CXX_FLAGS="${CXX_FLAGS}" -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:
- build/
-.template:fedora_build: &fedora_build
- <<: *basic_build
+.fedora_build:
+ extends: .basic_build
image: docker.louiz.org/louiz/biboumi/test-fedora:latest
build:fedora:
- <<: *fedora_build
+ extends: .fedora_build
build:debian:
- <<: *basic_build
+ extends: .basic_build
image: docker.louiz.org/louiz/biboumi/test-debian:latest
build:alpine:
+ extends: .basic_build
variables:
SYSTEMD: "-DWITHOUT_SYSTEMD=1"
- <<: *basic_build
image: docker.louiz.org/louiz/biboumi/test-alpine:latest
build:archlinux:
- <<: *basic_build
+ extends: .basic_build
only:
- - branches@louiz/biboumi
- when: manual
+ - schedules
tags:
- armv7l
artifacts:
paths: []
-build:1:
+build:freebsd:
+ extends: .basic_build
+ tags:
+ - freebsd
+ only:
+ - branches@louiz/biboumi
+ variables:
+ CXX_FLAGS: "-Werror"
+ SYSTEMD: "-DWITHOUT_SYSTEMD=1"
+ GIT_STRATEGY: "clone"
+ COMPILER: "clang++"
+
+build:no_botan:
+ extends: .fedora_build
variables:
BOTAN: "-DWITHOUT_BOTAN=1"
- <<: *fedora_build
-build:2:
+build:no_udns:
+ extends: .fedora_build
+ variables:
+ UDNS: "-DWITHOUT_UDNS=1"
+
+build:no_libidn:
+ extends: .fedora_build
variables:
UDNS: "-DWITHOUT_UDNS=1"
- <<: *fedora_build
-build:3:
+build:no_sqlite3:
+ extends: .fedora_build
variables:
SQLITE3: "-DWITHOUT_SQLITE3=1"
TEST_POSTGRES_URI: "postgres@postgres/postgres"
services:
- postgres:latest
- <<: *fedora_build
-build:4:
+build:no_db:
+ extends: .fedora_build
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_botan:
+ extends: .fedora_build
variables:
+ SQLITE3: "-DWITHOUT_SQLITE3=1"
+ POSTGRESQL: "-DWITHOUT_POSTGRESQL=1"
BOTAN: "-DWITHOUT_BOTAN=1"
- UDNS: "-DWITHOUT_UDNS=1"
- <<: *fedora_build
-
-build:without_udns:
- variables:
- UDNS: "-DWITHOUT_UDNS=1"
- <<: *fedora_build
#
## Test jobs
#
-.template:basic_test: &basic_test
+.basic_test:
+ extends: .sources_changed
stage: test
tags:
- docker
script:
- cd build/
- - make coverage_e2e -j$(nproc || echo 1)
- - make coverage
+ - make check_junit || true; make check;
+ - make e2e
artifacts:
- expire_in: 2 weeks
- paths:
- - build/coverage_test_suite/
- - build/coverage_e2e/
- - build/coverage_total/
- - build/coverage_e2e.info
- when: always
- name: $CI_PROJECT_NAME-test-$CI_JOB_ID
+ reports:
+ junit: ["build/check_result.xml"]
test:debian:
+ extends: .basic_test
image: docker.louiz.org/louiz/biboumi/test-debian:latest
- <<: *basic_test
dependencies:
- build:debian
+ needs: ["build:debian"]
test:fedora:
+ extends: .basic_test
image: docker.louiz.org/louiz/biboumi/test-fedora:latest
- <<: *basic_test
+ script:
+ - cd build/
+ - make coverage_check
+ - make coverage_e2e
+ - make coverage
dependencies:
- build:fedora
+ needs: ["build:fedora"]
+ artifacts:
+ expire_in: 2 weeks
+ paths:
+ - build/coverage_test_suite/
+ - build/coverage_e2e/
+ - build/coverage_total/
+ - build/coverage_e2e.info
+ when: always
+ name: $CI_PROJECT_NAME-test-$CI_JOB_ID
-test:without_udns:
+test:no_udns:
+ extends: .basic_test
image: docker.louiz.org/louiz/biboumi/test-fedora:latest
- <<: *basic_test
dependencies:
- - build:without_udns
+ - build:no_udns
+ needs: ["build:no_udns"]
test:alpine:
+ extends: .basic_test
image: docker.louiz.org/louiz/biboumi/test-alpine:latest
- stage: test
- tags:
- - docker
- script:
- - cd build/
- - make e2e
dependencies:
- build:alpine
+ needs: ["build:alpine"]
+ image: docker.louiz.org/louiz/biboumi/test-alpine:latest
test:freebsd:
- only:
- - branches@louiz/biboumi
+ extends: .basic_test
tags:
- freebsd
- variables:
- SYSTEMD: "-DWITHOUT_SYSTEMD=1"
- stage: test
- script:
- - mkdir build/
- - cd build/
- - cmake .. -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: []
+ dependencies:
+ - build:freebsd
+ needs: ["build:freebsd"]
#
## Packaging jobs
@@ -219,36 +216,11 @@ packaging:rpm:
dependencies:
- build:fedora
-packaging:deb:
- stage: packaging
- only:
- - master@louiz/biboumi
- - debian@louiz/biboumi
- tags:
- - docker
- allow_failure: true
- image: docker.louiz.org/louiz/biboumi/packaging-debian:latest
- before_script: []
- script:
- - git checkout debian
- - git pull
- - git merge --no-commit --no-ff master
- - mk-build-deps
- - apt update -y
- - apt install -y ./biboumi-build-deps*.deb
- - debuild -b -us -uc
- - mv ../*.deb .
- - mv ../*.build .
- - mv ../*.buildinfo .
- dependencies: []
- artifacts:
- untracked: true
- name: $CI_PROJECT_NAME-deb-$CI_BUILD_ID
-
packaging:archlinux:
stage: packaging
only:
- master@louiz/biboumi
+ - triggers
tags:
- docker
allow_failure: true
@@ -261,3 +233,55 @@ packaging:archlinux:
- makepkg -si --noconfirm
- test -e /usr/bin/biboumi
dependencies: []
+
+#
+# Deploy jobs
+#
+
+.doc_changed:
+ only:
+ changes:
+ - "CMakeLists.txt"
+ - ".gitlab-ci.yml"
+ - "doc/**/*"
+
+# The jobs with the secure tag need to access directories where important
+# files are stored: the latest doc, etc.'
+# Other jobs can not access these, otherwise anybody doing a merge request
+# could delete the official doc
+.deploy:doc:
+ extends: .doc_changed
+ stage: deploy
+ image: docker.louiz.org/louiz/biboumi/doc-builder
+ script:
+ - cd doc/
+ - make html
+ - rm -rf /www/$DOC_DEPLOY_DIR
+ - mv _build/html /www/$DOC_DEPLOY_DIR
+ dependencies: []
+
+deploy:doc:latest:
+ extends: .deploy:doc
+ only:
+ - master@louiz/biboumi
+ tags:
+ - www
+ - secure
+ environment:
+ name: doc.latest
+ url: https://doc.biboumi.louiz.org
+ variables:
+ DOC_DEPLOY_DIR: "latest"
+
+deploy:doc:tag:
+ extends: .deploy:doc
+ only:
+ - tags
+ tags:
+ - www
+ - secure
+ environment:
+ name: doc.$CI_COMMIT_TAG
+ url: https://doc.biboumi.louiz.org/$CI_COMMIT_TAG/
+ variables:
+ DOC_DEPLOY_DIR: $CI_COMMIT_TAG
diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index 22a65d9..a5d3f58 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -1,3 +1,36 @@
+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.
+- Support for XEP-0410 Self-Ping Optimization. This will prevent clients
+ which use self-ping from dropping out of the MUC if another client with
+ bad connectivity is also joined from the same account.
+
+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.
+- The WatchdogSec value in the biboumi.service file (for systemd) now
+ defaults to the empty string, which means “disabled”. This value can
+ still be set at configure time by passing the option "-DWATCHDOG_SEC=20”
+ to cmake, if you want to enable the systemd watchdog.
+
+For developers
+--------------
+- The end-to-end tests have been refactored, cleaned and simplified a lot.
+ A tutorial and a documentation have been written. It should now be easy
+ to write a test that demonstrates a bug or a missing feature.
+
Version 8.4 - 2020-02-25
========================
diff --git a/CMakeLists.txt b/CMakeLists.txt
index b3c5d99..ac86555 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -2,9 +2,9 @@ cmake_minimum_required(VERSION 3.0)
project(biboumi)
-set(${PROJECT_NAME}_VERSION_MAJOR 8)
-set(${PROJECT_NAME}_VERSION_MINOR 4)
-set(${PROJECT_NAME}_VERSION_SUFFIX "")
+set(${PROJECT_NAME}_VERSION_MAJOR 9)
+set(${PROJECT_NAME}_VERSION_MINOR 0)
+set(${PROJECT_NAME}_VERSION_SUFFIX "~dev")
if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
if (CMAKE_CXX_COMPILER_VERSION VERSION_LESS 5.0)
@@ -74,26 +74,11 @@ 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(html COMMAND make html BUILDDIR=${CMAKE_CURRENT_BINARY_DIR}
+ WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/doc)
+add_custom_target(man COMMAND make man BUILDDIR=${CMAKE_CURRENT_BINARY_DIR}
+ WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/doc)
+add_custom_target(doc DEPENDS html man)
#
## Set this search path for cmake, to find our custom search modules
@@ -218,8 +203,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 +218,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 +231,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
@@ -327,7 +312,9 @@ endif()
## Add some custom rules to launch the tests
#
add_custom_target(check COMMAND "test_suite"
- DEPENDS test_suite biboumi)
+ DEPENDS test_suite)
+add_custom_target(check_junit COMMAND test_suite -r junit -o check_result.xml
+ DEPENDS test_suite)
set_target_properties(check PROPERTIES EXCLUDE_FROM_ALL TRUE)
add_custom_target(e2e COMMAND "python3" "${CMAKE_CURRENT_SOURCE_DIR}/tests/end_to_end/"
DEPENDS biboumi)
@@ -338,13 +325,14 @@ if(CMAKE_BUILD_TYPE MATCHES Debug)
include(CodeCoverage)
SETUP_TARGET_FOR_COVERAGE(coverage_check
./test_suite
- coverage_test_suite)
+ coverage_test_suite
+ "-o;check_result.xml;-r;junit")
add_dependencies(coverage_check test_suite)
SETUP_TARGET_FOR_COVERAGE(coverage_e2e
python3
coverage_e2e
- ${CMAKE_CURRENT_SOURCE_DIR}/tests/end_to_end/)
+ "${CMAKE_CURRENT_SOURCE_DIR}/tests/end_to_end/")
add_dependencies(coverage_e2e biboumi)
ADD_CUSTOM_TARGET(coverage
@@ -361,7 +349,7 @@ add_custom_target(everything DEPENDS test_suite biboumi)
## Install target
#
install(TARGETS ${PROJECT_NAME} RUNTIME DESTINATION bin)
-install(FILES ${MAN_PAGE} DESTINATION share/man/man1 OPTIONAL COMPONENT documentation)
+install(FILES ${CMAKE_CURRENT_BINARY_DIR}/man/biboumi.1 DESTINATION share/man/man1 OPTIONAL COMPONENT documentation)
install(FILES ${CMAKE_CURRENT_BINARY_DIR}/biboumi.service DESTINATION lib/systemd/system COMPONENT init)
file(GLOB policy_files conf/*policy.txt)
install(FILES ${policy_files} DESTINATION /etc/biboumi COMPONENT configuration)
@@ -397,12 +385,11 @@ add_custom_target(rpm
#
set(SYSTEMD_SERVICE_TYPE_DOCSTRING "The value used as the Type= in the systemd unit file.")
set(WATCHDOG_SEC_DOCSTRING "The value used as WatchdogSec= in the systemd unit file.")
+set(WATCHDOG_SEC "" CACHE STRING ${WATCHDOG_SEC_DOCSTRING})
if(SYSTEMD_FOUND)
set(SYSTEMD_SERVICE_TYPE "notify" CACHE STRING ${SYSTEMD_SERVICE_TYPE_DOCSTRING})
- set(WATCHDOG_SEC "20" CACHE STRING ${WATCHDOG_SEC_DOCSTRING})
else()
set(SYSTEMD_SERVICE_TYPE "simple" CACHE STRING ${SYSTEMD_SERVICE_TYPE_DOCSTRING})
- set(WATCHDOG_SEC "" CACHE STRING ${WATCHDOG_SEC_DOCSTRING})
endif()
set(SERVICE_USER_DOCSTRING "The value used as the User= in the systemd unit file.")
if(NOT DEFINED SERVICE_USER)
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/biboumi.cfg b/conf/biboumi.cfg
index e6b8ed5..8e51542 100644
--- a/conf/biboumi.cfg
+++ b/conf/biboumi.cfg
@@ -1,7 +1,15 @@
-hostname=biboumi.example.com
-password=secret
-db_name=/var/lib/biboumi/biboumi.sqlite
-log_file=/var/log/biboumi/biboumi.log
-log_level=0
-admin=
+# This is an example configuration for the biboumi component.
+# It only contains the default values.
+# You need to, at least, set the values for hostname and password, if you
+# want biboumi to be able to start
+
+hostname=
+password=
+xmpp_server_ip=127.0.0.1
port=5347
+admin=
+realname_customization=true
+realname_from_jid=false
+log_file=
+ca_file=
+outgoing_bind=
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/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 03e8a36..0000000
--- a/doc/biboumi.1.rst
+++ /dev/null
@@ -1,762 +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.
-
-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 `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>`_.
-
-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.
-
-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.
-
-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 (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:
-
- * 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 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.
-
-- 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..c607fc5
--- /dev/null
+++ b/doc/conf.py
@@ -0,0 +1,146 @@
+# -*- 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'
+
+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',
+ author, 'manual'),
+]
+
+
+# -- Options for manual page output ------------------------------------------
+
+# One entry per manual page. List of tuples
+# (source start file, name, description, authors, manual section).
+man_pages = [
+ ('man_index', 'biboumi', 'biboumi Documentation',
+ [author], 1)
+]
+
+# -- Extension configuration -------------------------------------------------
diff --git a/CONTRIBUTING.rst b/doc/contributing.rst
index 8df4899..8f01c82 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
@@ -45,31 +50,13 @@ Tests
There are two test suites for biboumi:
- unit tests that can be run simply using `make check`.
- These tests use the Catch test framework, are written in pure C++
+ These tests use the Catch2 test framework, are written in pure C++
and they should always succeed, in all possible build configuration.
- a more complex end-to-end test suite. This test suite is written in python3,
uses a specific IRC server (`charybdis`_), and only tests the most complete
- biboumi configuration (when all dependencies are used). To run it, you need
- to install various dependencies: refer to fedora’s `Dockerfile.base`_ and
- `Dockerfile`_ to see how to install charybdis, slixmpp, botan, a ssl
- certificate, etc.
-
- Once all the dependencies are correctly installed, the tests are run with
-
- `make e2e`
-
- To run one or more specific tests, you can do something like this:
-
- `make biboumi && python3 ../tests/end_to_end self_ping 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.
-
+ biboumi configuration (when all dependencies are used).
+ Read more about these tests in the specific documentation TODO.
All these tests automatically run with various configurations, on various
platforms, using gitlab CI.
@@ -82,7 +69,7 @@ Please try to follow the existing style:
- Use only spaces, not tabs.
- Curly brackets are on their own lines.
- Use this-> everywhere it’s possible.
-- Don’t start class attributes with “m_” or similar.
+- Don’t start class attributes with “m\_” or similar.
- Type names are in PascalCase.
- Everything else is in snake_case.
diff --git a/doc/developer.rst b/doc/developer.rst
new file mode 100644
index 0000000..b3ef158
--- /dev/null
+++ b/doc/developer.rst
@@ -0,0 +1,302 @@
+########################
+Developer documentation
+########################
+
+End-to-end test suite
+---------------------
+
+A powerful test suite has been developped to test biboumi’s behaviour in
+many scenarios. Its goal is to simulate a real-world usage of biboumi,
+including its interactions with a real IRC server an a real XMPP client.
+
+An IRC server is started, with a specific version and configuration, then,
+for every scenario that we want to test:
+
+- Biboumi is started, with a specific configuration
+- An XMPP “client” starts, communicates with biboumi and checks that
+ biboumi responds in the expected way.
+
+The XMPP client is actually not a real client, it’s a python script that
+uses the slixmpp library to imitate an XMPP server that would transmit the
+stanzas of one client to its component (biboumi). In real life, the
+communication to biboumi is done between the XMPP server and biboumi, but
+since the server just forwards the messages that clients send unmodified,
+we’ll call that “the clients”.
+
+A scenario is a list of functions that will be executed one by one, to
+verify the behaviour of one specific feature. Ideally, they should be
+short and test one specific aspect.
+
+Run the test suite locally
+~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Since this requires a lot of dependencies (an IRC server with some TLS
+certificate, slixmpp, many libraries…), it might be cumbersome to get
+everything on your machine to be able to run them.
+
+The simplest solution (as long as you have docker installed and properly
+configured to be able to run as your developer user… It’s as simple as
+“dnf install docker” and “chmod o+rw /var/run/docker.sock”, but that’s not
+recommended, because this lets anybody on the system use docker, and
+docker is very unsecure) is to follow these instructions:
+
+.. code-block:: bash
+ :caption: Start a docker container with everything installed
+
+ docker run --name biboumi-e2e -v /home/louiz/biboumi/:/home/tester/biboumi \
+ --add-host="irc.localhost:127.0.0.1" \
+ --add-host="biboumi.localhost:127.0.0.1" \
+ --rm -it docker.louiz.org/louiz/biboumi/test-alpine \
+ /bin/bash
+
+This creates a container where every dependency is already installed. We
+mount your working directory inside the container: be sure to modify the
+first path `/home/louiz/biboumi` with your own. The hosts that we add are
+needed for the test suite to properly work.
+
+You can use the test-fedora or test-debian images instead of test-alpine
+if you want, but it should not change anything (even if your host machine
+uses debian or fedora), alpine is just the lighter one.
+
+.. note::
+
+ This container should stay alive as long as you want to run the test
+ suite. For example if you want to run it many times until your code is
+ fine and all tests pass, just leave that shell somewhere without
+ touching it.
+
+Then, from an other shell (do NOT run that inside the container we just
+created):
+
+.. code-block:: bash
+ :caption: Configure and build biboumi from inside the container
+
+ docker exec biboumi-e2e sh -c "cd biboumi && mkdir docker-build/ && cd docker-build/ && cmake .."
+
+This is needed (only once), because if you configure it from your host
+machine, then the paths generated by cmake will be all wrong when you try
+to compile from inside the container and nothing will work.
+
+.. code-block:: bash
+ :caption: Re-compile and run the test suite inside the container
+
+ docker exec biboumi-e2e sh -c "cd biboumi/docker-build && make e2e"
+
+This should now build everything correctly, and run the test suite. If you
+want to re-run it again after you edited something in your source tree,
+just run this last command again. You don’t need to touch anything inside
+the container again.
+
+When you’re done, just close the shell we opened with the first command.
+
+Available functions
+~~~~~~~~~~~~~~~~~~~
+
+.. py:function:: send_stanza(str)
+
+ sends one stanza to biboumi. The stanza is written entirely
+ as a string (with a few automatic replacements). The “from” and “to”
+ values have to be specified everytime, because each stanza can come from
+ different clients and be directed to any IRC server/channel
+
+ .. code-block:: python
+
+ send_stanza("<message from='{jid_one}/{resource_one}' to='#foo@{biboumi_host}' type='groupchat'><body>coucou</body></message>"),
+
+.. py:function:: expect_stanza(xpath[, …])
+
+ Waits for a stanza to be received by biboumi, and checks that this
+ stanza matches one or more xpath. If the stanza doesn’t match all the
+ given xpaths, then the scenario ends and we report that as an error.
+
+ .. code-block:: python
+
+ expect_stanza("/message[@from='#foo@{biboumi_host}/{nick_one}']/body[text()='coucou']",
+ "/message/delay:delay[@from='#foo@{biboumi_host}']"),
+
+ This waits for exactly 1 stanza, that is compared against 2 xpaths. Here
+ we check that it is a message, that it has the proper `from` value, the
+ correct body, and a <delay/>.
+
+.. py:function:: expect_unordered(list_of_xpaths[, list_of_xpaths, …])
+
+ we wait for more than one stanzas, that could be received in any order.
+ For example, in certain scenario, we wait for two presence stanzas, but
+ it’s perfectly valid to receive them in any order (one is for one
+ client, the other one for an other client). To do that, we pass multiple
+ lists of xpath. Each list can contain one or more xpath (just like
+ `expect_stanza`). When a stanza is received, it is compared with all the
+ xpaths of the first list. If it doesn’t match, it is compared with the
+ xpaths of the second list, and so on. If nothing matchs, it’s an error
+ and we stop this scenario. If the stanza matches with one of the xpath
+ lists, we remove that list, and we wait for the next stanza, until there
+ are no more xpaths.
+
+ .. code-block:: python
+
+ 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']",
+ ],
+ ),
+
+ This will wait for 4 stanzas that could be received in any order.
+
+To avoid many repetitions between each tests, some helpful sequences are
+available, `sequences.connection(…)` and `sequences.connection_tls(…)`.
+They do all the steps that are needed (send and receive stanzas) to
+connect to the component, or an IRC server.
+
+It’s also possible to reuse one simple scenario into an other scenario.
+The most notable example is to start your own scenario with
+`scenarios.simple_channel_join.scenario`, if you need your client to be in
+a channel before you can start your actual scenario. For example if you
+want to test the behaviour of a topic change, you need to first join a
+channel. Since this is a very common patern, it’s simpler to just included
+this very basic scenario at the start of your own scenarios, instead of
+copy pasting the same thing over and over.
+
+Examples of a scenario
+~~~~~~~~~~~~~~~~~~~~~~
+
+First example
+^^^^^^^^^^^^^
+
+Here we’ll describe how to write your own scenario, from scratch. For this, we will take an existing scenario and explain how it was written, line by line.
+
+See for example the scenario tests/end_to_end/scenarios/self_ping_on_real_channel.py
+
+.. code-block:: python
+
+ from scenarios import *
+
+All the tests should start with this import. It imports the file
+tests/end_to_end/scenarios/__init__.py This make all the functions
+available (send_stanza, expect_stanza…) available, as well as some very
+common scenarios that you often need to re-use.
+
+.. code-block:: python
+
+ scenario = (
+ # …
+ )
+
+This is the only required element of your scenario. This object is a tuple of function calls OR other scenarios.
+
+.. code-block:: python
+
+ scenarios.simple_channel_join.scenario,
+
+The first line of our scenario is actually including an other existing
+scenario. You can find it at
+tests/end_to_end/scenarios/simple_channel_join.py As its name shows, it’s
+very basic: one client {jid_one}/{resource_one} just joins one room
+#foo%{irc_server_one} with the nick {nick_one}.
+
+Since we want to test the behaviour of a ping to ourself when we are in a
+room, we just join this room without repeating everything.
+
+It is possible to directly insert a scenario inside our scenario without
+having to extract all the steps: the test suite is smart enough to detect
+that and extract the inner steps automatically.
+
+.. code-block:: python
+
+ # Send a ping to ourself
+ send_stanza("<iq type='get' from='{jid_one}/{resource_one}' id='first_ping' to='#foo%{irc_server_one}/{nick_one}'><ping xmlns='urn:xmpp:ping' /></iq>"),
+ expect_stanza("/iq[@from='#foo%{irc_server_one}/{nick_one}'][@type='result'][@to='{jid_one}/{resource_one}'][@id='first_ping']"),
+
+Here we simple send an iq stanza, properly formatted, using the same JIDs
+{jid_one}/{resource_one} and #foo%{irc_server_one}/{nick_one} to ping
+ourself in the room. We them immediately expect one stanza to be received,
+that is the response to our ping. It only contains one single xpath
+because everything we need to check can be expressed in one line.
+
+Note that it is recommended to explain all the steps of your scenario with
+comments. This helps understand what is being tested, and why, without
+having to analyze all the stanza individually.
+
+.. code-block:: python
+
+ # Now join the same room, from the same bare JID, behind the same nick
+ send_stanza("<presence from='{jid_one}/{resource_two}' to='#foo%{irc_server_one}/{nick_one}' />"),
+ expect_stanza("/presence[@to='{jid_one}/{resource_two}'][@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']"),
+
+ expect_stanza("/message[@from='#foo%{irc_server_one}'][@type='groupchat'][@to='{jid_one}/{resource_two}']/subject[not(text())]"),
+
+Here we send a presence stanza to join the same channel with an other
+resource (note the {resource_two}). As a result, we expect two stanzas:
+The first stanza (our self-presence) is checked against two xpaths, and
+the second stanza (the empty subject of the room) against only one.
+
+.. code-block:: python
+
+ # And re-send a self ping
+ send_stanza("<iq type='get' from='{jid_one}/{resource_one}' id='second_ping' to='#foo%{irc_server_one}/{nick_one}'><ping xmlns='urn:xmpp:ping' /></iq>"),
+ expect_stanza("/iq[@from='#foo%{irc_server_one}/{nick_one}'][@type='result'][@to='{jid_one}/{resource_one}'][@id='second_ping']"),
+ ## And re-do exactly the same thing, just change the resource initiating the self ping
+ send_stanza("<iq type='get' from='{jid_one}/{resource_two}' id='third_ping' to='#foo%{irc_server_one}/{nick_one}'><ping xmlns='urn:xmpp:ping' /></iq>"),
+ expect_stanza("/iq[@from='#foo%{irc_server_one}/{nick_one}'][@type='result'][@to='{jid_one}/{resource_two}'][@id='third_ping']"),
+
+And finally, we test a second ping, and check that the behaviour is correct that we now have two resources in that channel.
+
+Second example
+^^^^^^^^^^^^^^
+
+Sometimes we want to do more with the received stanzas. For example we
+need to extract some values from the received stanzas, to reuse them in
+future stanzas we send. The most obvious example is iq IDs, that we need
+to extract, to reuse them in our response.
+
+Let’s use for example the tests/end_to_end/scenarios/execute_incomplete_hello_adhoc_command.py scenario:
+
+.. code-block:: python
+
+ from scenarios import *
+
+ scenario = (
+ send_stanza("<iq type='set' id='hello-command1' from='{jid_one}/{resource_one}' to='{biboumi_host}'><command xmlns='http://jabber.org/protocol/commands' node='hello' action='execute' /></iq>"),
+ expect_stanza("/iq[@type='result']/commands:command[@node='hello'][@sessionid][@status='executing']",
+ "/iq/commands:command/commands:actions/commands:complete",
+ after = save_value("sessionid", extract_attribute("/iq[@type='result']/commands:command[@node='hello']", "sessionid"))
+ ),
+
+Here is where the magic happens: as an additional argument to the
+expect_stanza function, we pass an other function (callback) with the
+“after=” keyword argument. This “after” callback gets called once the
+expected stanza has been received and validated. Here we use
+`save_value(key, value)`. This function just saves a value in our global
+values that can be used with “send_stanza”, associated with the given
+“key”. For example if you do `save_value("something_important", "blah")`
+then you can use `{something_important}` in any future stanza that you
+send and it will be replaced with “blah”.
+
+But this is only useful if we can save some value that we extract from the
+stanza. That’s where `extract_attribute(xpath, attribute_name)` comes into
+play. As the first argument, you pass an xpath corresponding to one
+specific node of the XML that is received, and the second argument is just
+the name of the attribute whose value you want.
+
+Here, we extract the value of the “sessionid=” in the node `<iq
+type='result'><commands:command node='hello' sessionid='…' /></iq>`, and
+we save that value, globally, with the name “sessionid”.
+
+.. code-block:: python
+
+ send_stanza("<iq type='set' id='hello-command2' from='{jid_one}/{resource_one}' to='{biboumi_host}'><command xmlns='http://jabber.org/protocol/commands' node='hello' sessionid='{sessionid}' action='complete'><x xmlns='jabber:x:data' type='submit'></x></command></iq>"),
+
+Here we send a second iq, to continue our ad-hoc command, and we use {sessionid} to indicate that we are continuing the session we started before.
diff --git a/doc/example.conf b/doc/example.conf
deleted file mode 100644
index 42631b2..0000000
--- a/doc/example.conf
+++ /dev/null
@@ -1,14 +0,0 @@
-# This is an example configuration for the biboumi component.
-# It only contains the default values, and some example values for the
-# required fields (hostname and password).
-
-hostname=biboumi.example.com
-password=mypassword
-xmpp_server_ip=127.0.0.1
-port=5347
-admin=
-realname_customization=true
-realname_from_jid=false
-log_file=
-ca_file=
-outgoing_bind=
diff --git a/doc/index.rst b/doc/index.rst
new file mode 100644
index 0000000..819a3e5
--- /dev/null
+++ b/doc/index.rst
@@ -0,0 +1,24 @@
+.. 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
+ developer
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/man_index.rst b/doc/man_index.rst
new file mode 100644
index 0000000..3de981d
--- /dev/null
+++ b/doc/man_index.rst
@@ -0,0 +1,9 @@
+Biboumi's man page index
+========================
+
+.. toctree::
+ :maxdepth: 2
+ :caption: Contents:
+
+ synopsis
+ admin
diff --git a/doc/synopsis.rst b/doc/synopsis.rst
new file mode 100644
index 0000000..5b93a92
--- /dev/null
+++ b/doc/synopsis.rst
@@ -0,0 +1,4 @@
+Synopsis
+========
+
+biboumi [*config_filename*]
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/alpine/Dockerfile b/docker/biboumi-test/alpine/Dockerfile
index e43f1b6..9d59c32 100644
--- a/docker/biboumi-test/alpine/Dockerfile
+++ b/docker/biboumi-test/alpine/Dockerfile
@@ -26,6 +26,7 @@ RUN apk add --no-cache g++\
python3-dev\
automake\
autoconf\
+ libffi-dev\
flex\
bison\
libltdl\
@@ -48,7 +49,8 @@ RUN git clone https://github.com/charybdis-ircd/charybdis.git && cd charybdis &&
RUN chown -R tester:tester /home/tester/ircd
+USER tester
RUN yes "" | openssl req -nodes -x509 -newkey rsa:4096 -keyout /home/tester/ircd/etc/ssl.key -out /home/tester/ircd/etc/ssl.pem
WORKDIR /home/tester
-USER tester
+
diff --git a/docker/biboumi-test/debian/Dockerfile b/docker/biboumi-test/debian/Dockerfile
index 557face..1c6437c 100644
--- a/docker/biboumi-test/debian/Dockerfile
+++ b/docker/biboumi-test/debian/Dockerfile
@@ -1,7 +1,7 @@
# This Dockerfile creates a docker image suitable to run biboumi’s build and
# tests. For example, it can be used on with gitlab-ci.
-FROM docker.io/debian:latest
+FROM docker.io/debian:buster
ENV LC_ALL C.UTF-8
@@ -22,8 +22,8 @@ RUN apt install -y g++\
libidn11-dev\
uuid-dev\
libsystemd-dev\
- pandoc\
- libasan3\
+ python3-sphinx\
+ libasan5\
libubsan0\
git\
python3-lxml\
@@ -46,7 +46,7 @@ RUN apt install -y g++\
RUN git clone https://github.com/randombit/botan.git && cd botan && ./configure.py --prefix=/usr && make -j8 && make install && rm -rf /botan
# Install slixmpp, for e2e tests
-RUN git clone https://github.com/saghul/aiodns.git && cd aiodns && git checkout 7ee13f9bea25784322~ && python3 setup.py build && python3 setup.py install && git clone git://git.louiz.org/slixmpp && pip3 install pyasn1==0.4.2 && cd slixmpp && python3 setup.py build && python3 setup.py install
+RUN git clone https://github.com/saghul/aiodns.git && cd aiodns && git checkout 7ee13f9bea25784322~ && python3 setup.py build && python3 setup.py install && git clone git://git.louiz.org/slixmpp && pip3 install pyasn1 && cd slixmpp && python3 setup.py build && python3 setup.py install
RUN useradd tester -m
diff --git a/docker/biboumi-test/fedora/Dockerfile b/docker/biboumi-test/fedora/Dockerfile
index 12e13e5..61fa3be 100644
--- a/docker/biboumi-test/fedora/Dockerfile
+++ b/docker/biboumi-test/fedora/Dockerfile
@@ -20,7 +20,7 @@ RUN dnf --refresh install -y\
libidn-devel\
uuid-devel\
systemd-devel\
- pandoc\
+ python3-sphinx\
libasan\
libubsan\
git\
@@ -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/images/biboumi.svg b/images/biboumi.svg
new file mode 100644
index 0000000..0e6b625
--- /dev/null
+++ b/images/biboumi.svg
@@ -0,0 +1,31 @@
+<?xml version="1.0"?>
+<svg version="1.1" viewBox="0 0 200 200" xmlns="http://www.w3.org/2000/svg">
+ <defs>
+ <clipPath id="b">
+ <circle cx="82.809" cy="196.97" r="43.879" fill="#ff7c0f" fill-opacity=".76078" stroke-width=".26458"/>
+ </clipPath>
+ <clipPath id="d">
+ <path d="m180.52 259.24-37.156-4.869c-12.066-10.349-20.924-19.652-26.575-27.907-5.6403-8.171-8.9557-16.371-9.9462-24.601-1.1233-9.3327 1.6749-19.655 8.3945-30.966 0.44431-0.86032 0.7801-1.4844 1.0074-1.8721 0.346-0.53919 0.79032-1.3995 1.333-2.581 7.0452-12.02 10.088-22.018 9.1279-29.993-0.89862-7.4661-4.5007-15.203-10.806-23.211-6.1663-7.9894-15.525-16.898-28.076-26.726l37.156 4.869c12.066 10.349 20.919 19.609 26.559 27.78 5.7897 8.2741 34.422 7.0601 35.413 15.29 1.1233 9.3327 28.762 21.791 21.913 33.169-0.45452 0.77548 25.236 0.0424 25.008 0.43013 83.542 6.6717-29.704 19.89-30.256 20.987-7.0452 12.02-29.545 19.651-28.585 27.626 0.8782 7.2964-29.003 7.4922-22.857 15.312 6.1561 7.9046 15.605 16.992 28.346 27.263z" fill="#fc0" stroke-width="8.2789"/>
+ </clipPath>
+ <clipPath id="a">
+ <circle cx="82.809" cy="196.97" r="43.879" fill="#ff7c0f" fill-opacity=".76078" stroke-width=".26458"/>
+ </clipPath>
+ <clipPath id="c">
+ <path transform="matrix(.094728 .78705 -1.2909 -.16917 0 0)" d="m296-63.677v28.782c-11.319 8.5161-21.829 14.607-31.531 18.272-9.594 3.6651-3.2417 31.956-13.698 31.956-11.858 0-29.785-0.63152-45.524-6.9916-1.1858-0.43119-18.636-1.7681-19.175-1.9837-0.75459-0.3234-31.977 22.705-22.21-1.0329-16.709-6.6835-5.957-9.4736-16.09-9.4736-9.4863 0-20.045-26.704-29.316-22.5-9.2707 4.0963-19.188 10.618-29.752 19.565v-28.782c11.319-8.5161 21.775-14.607 31.369-18.272 9.7018-3.7729 19.781-5.6594 30.237-5.6594 11.858 0 25.71 3.234 41.556 9.7019 1.078 0.43119 1.8865 0.75459 2.4255 0.97019 0.86239 0.3234 2.0482 0.75459 3.5574 1.2936 16.709 6.6835 30.13 10.025 40.263 10.025 9.2707 0 18.434-2.0482 27.489-6.1445 9.1629-4.0963 19.296-10.672 30.399-19.727z" fill="#d45500" stroke-width="8.2789"/>
+ </clipPath>
+ </defs>
+ <g transform="translate(0,-97)">
+ <g transform="matrix(1.5456 0 0 1.7393 -48.852 -89.968)" clip-path="url(#c)">
+ <circle cx="89.102" cy="164.99" r="57.494" fill="#f0c" fill-opacity=".76078" stroke-width=".32058"/>
+ <circle cx="109.9" cy="164.99" r="57.494" fill="#ff7c0f" fill-opacity=".76078" stroke-width=".32058"/>
+ <circle transform="matrix(1.4205 0 0 1.3103 -6.6791 -93.099)" cx="66.934" cy="196.97" r="43.879" clip-path="url(#b)" fill="#333" fill-opacity=".25521" stroke-width=".24466"/>
+ <circle transform="matrix(1.3103 0 0 1.3103 1.4023 -93.099)" cx="66.934" cy="196.97" r="43.879" clip-path="url(#b)" fill="#333" stroke-width=".24466"/>
+ </g>
+ <g transform="matrix(1.5456 0 0 1.7393 -58.728 -89.971)" clip-path="url(#d)">
+ <circle cx="89.102" cy="164.99" r="57.494" fill="#f0c" fill-opacity=".76078" stroke-width=".32058"/>
+ <circle cx="109.9" cy="164.99" r="57.494" fill="#ff7c0f" fill-opacity=".76078" stroke-width=".32058"/>
+ <circle transform="matrix(1.4205 0 0 1.3103 -6.6791 -93.099)" cx="66.934" cy="196.97" r="43.879" clip-path="url(#a)" fill="#333" fill-opacity=".25521" stroke-width=".24466"/>
+ <circle transform="matrix(1.3103 0 0 1.3103 1.4023 -93.099)" cx="66.934" cy="196.97" r="43.879" clip-path="url(#a)" fill="#333" stroke-width=".24466"/>
+ </g>
+ </g>
+</svg>
diff --git a/packaging/biboumi.spec.cmake b/packaging/biboumi.spec.cmake
index 0c2a69b..4a87bb0 100644
--- a/packaging/biboumi.spec.cmake
+++ b/packaging/biboumi.spec.cmake
@@ -13,9 +13,10 @@ BuildRequires: libuuid-devel
BuildRequires: systemd-devel
BuildRequires: sqlite-devel
BuildRequires: postgresql-devel
+BuildRequires: botan2-devel
BuildRequires: cmake
BuildRequires: systemd
-BuildRequires: pandoc
+BuildRequires: python3-sphinx
%global _hardened_build 1
@@ -37,14 +38,14 @@ 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 \
-DWITH_POSTGRESQL=1
make %{?_smp_mflags}
-
+make man SPHINXBUILD=sphinx-build-3
%install
make install DESTDIR=%{buildroot}
@@ -57,22 +58,27 @@ 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
%changelog
+* ${RPM_DATE} Le Coz Florent <louiz@louiz.org> - ${RPM_VERSION}-1
+- Build latest git revision
+- Build against botan2
+- Build with sphinx instead of pandoc
+
* Tue Feb 25 2020 Le Coz Florent <louiz@louiz.org> - 8.4-1
Update to version 8.4
* 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
+* Fri 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
+* Mon 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
@@ -81,7 +87,7 @@ make check %{?_smp_mflags}
* Wed Jan 24 2018 Le Coz Florent <louiz@louiz.org> - 7.2-1
Update to version 7.2
-* Wed Jan 22 2018 Le Coz Florent <louiz@louiz.org> - 7.1-1
+* Mon Jan 22 2018 Le Coz Florent <louiz@louiz.org> - 7.1-1
Update to version 7.1
* Wed Jan 17 2018 Le Coz Florent <louiz@louiz.org> - 7.0-1
diff --git a/src/bridge/bridge.cpp b/src/bridge/bridge.cpp
index 7a0157a..71c0ea4 100644
--- a/src/bridge/bridge.cpp
+++ b/src/bridge/bridge.cpp
@@ -63,7 +63,8 @@ void Bridge::shutdown(const std::string& exit_message)
{
for (auto& pair: this->irc_clients)
{
- pair.second->send_quit_command(exit_message);
+ std::unique_ptr<IrcClient>& irc = pair.second;
+ irc->send_quit_command(exit_message);
}
}
@@ -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();
}
}
@@ -223,6 +224,27 @@ void Bridge::send_channel_message(const Iid& iid, const std::string& body, std::
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) {
+ std::string line = message.arguments[1];
+ // “temporary” workaround for \01ACTION…\01 -> /me messages
+ if ((line.size() > strlen("\01ACTION\01")) &&
+ (line.substr(0, 7) == "\01ACTION") && line[line.size() - 1] == '\01')
+ line = "/me " + line.substr(8, line.size() - 9);
+ 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);
@@ -231,22 +253,11 @@ void Bridge::send_channel_message(const Iid& iid, const std::string& body, std::
// 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
- if (!first || id.empty())
- id = utils::gen_uuid();
- 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);
first = false;
}
}
@@ -449,9 +460,8 @@ void Bridge::leave_irc_channel(Iid&& iid, const std::string& status_message, con
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)
@@ -737,10 +747,27 @@ void Bridge::send_irc_participant_ping_request(const Iid& iid, const std::string
IrcChannel* chan = irc->get_channel(iid.get_local());
if (!chan->joined || !this->is_resource_in_chan(iid.to_tuple(), from.resource))
{
- this->xmpp.send_stanza_error("iq", to_jid, from_jid, iq_id, "cancel", "not-allowed",
+ this->xmpp.send_stanza_error("iq", to_jid, from_jid, iq_id, "cancel", "not-acceptable",
"", true);
return;
}
+ if (chan->get_self()->nick == nick)
+ {
+ // XEP-0410 self-ping optimisation: always reply without going the full
+ // round-trip through IRC and possibly another XMPP client. See the XEP
+ // for details.
+ Jid iq_from(from_jid);
+ iq_from.local = std::to_string(iid);
+ iq_from.resource = nick;
+
+ Stanza iq("iq");
+ iq["from"] = iq_from.full();
+ iq["to"] = to_jid;
+ iq["id"] = iq_id;
+ iq["type"] = "result";
+ this->xmpp.send_stanza(iq);
+ return;
+ }
if (chan->get_self()->nick != nick && !chan->find_user(nick))
{
this->xmpp.send_stanza_error("iq", to_jid, from_jid, iq_id, "cancel", "item-not-found",
@@ -816,7 +843,7 @@ 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{};
@@ -824,9 +851,11 @@ void Bridge::send_message(const Iid& iid, const std::string& nick, const std::st
{
#ifdef USE_DATABASE
const auto xmpp_body = this->make_xmpp_body(body, encoding);
- if (!nick.empty() && this->record_history)
+ 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()])
{
@@ -891,9 +920,7 @@ void Bridge::send_muc_leave(const Iid& iid, const IrcUser& user,
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)
@@ -967,8 +994,18 @@ void Bridge::send_user_join(const std::string& hostname, const std::string& chan
std::string encoded_chan_name(chan_name);
xep0106::encode(encoded_chan_name);
- this->xmpp.send_user_join(encoded_chan_name + utils::empty_if_fixed_server("%" + hostname), user->nick, user->host,
- affiliation, role, this->user_jid + "/" + resource, self);
+ std::string encoded_nick_name(user->nick);
+ xep0106::encode(encoded_nick_name);
+
+ std::string full_jid =
+ encoded_nick_name + utils::empty_if_fixed_server("%" + hostname)
+ + "@" + this->xmpp.get_served_hostname();
+ if (!user->host.empty())
+ full_jid += "/" + user->host;
+
+ this->xmpp.send_user_join(encoded_chan_name + utils::empty_if_fixed_server("%" + hostname),
+ user->nick, full_jid, affiliation, role,
+ this->user_jid + "/" + resource, self);
}
void Bridge::send_topic(const std::string& hostname, const std::string& chan_name, const std::string& topic,
@@ -1001,11 +1038,13 @@ 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 result = Database::get_muc_logs(this->user_jid, chan_name, hostname, limit, history_limit.since, {}, Id::unset_value, Database::Paging::last);
+ 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)
@@ -1142,12 +1181,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;
}
@@ -1214,15 +1253,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);
diff --git a/src/bridge/bridge.hpp b/src/bridge/bridge.hpp
index 8e7d9d7..fa2a31f 100644
--- a/src/bridge/bridge.hpp
+++ b/src/bridge/bridge.hpp
@@ -164,9 +164,9 @@ public:
void send_room_history(const std::string& hostname, const std::string& chan_name, const HistoryLimit& history_limit);
void send_room_history(const std::string& hostname, std::string chan_name, const std::string& resource, const HistoryLimit& history_limit);
/**
- * Send a MUC message from some participant
+ * Send a message from a MUC participant or a direct message
*/
- 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.
*/
@@ -241,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);
@@ -275,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
*/
@@ -316,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/database/count_query.hpp b/src/database/count_query.hpp
index 118ce44..3990eb9 100644
--- a/src/database/count_query.hpp
+++ b/src/database/count_query.hpp
@@ -8,10 +8,10 @@
struct CountQuery: public Query
{
- CountQuery(std::string name):
+ CountQuery(const std::string& name):
Query("SELECT count(*) FROM ")
{
- this->body += std::move(name);
+ this->body += name;
}
int64_t execute(DatabaseEngine& db)
diff --git a/src/database/database.cpp b/src/database/database.cpp
index 6e08ee1..861abcb 100644
--- a/src/database/database.cpp
+++ b/src/database/database.cpp
@@ -162,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;
}
@@ -338,7 +334,6 @@ Transaction::Transaction()
log_error("Failed to create SQL transaction: ", std::get<std::string>(result));
else
this->success = true;
-
}
Transaction::~Transaction()
diff --git a/src/database/database.hpp b/src/database/database.hpp
index 3e25b30..a53f87b 100644
--- a/src/database/database.hpp
+++ b/src/database/database.hpp
@@ -63,8 +63,8 @@ class Database
struct EncodingIn: Column<std::string> { static constexpr auto name = "encodingin_"; };
- struct MaxHistoryLength: Column<int> { static constexpr auto name = "maxhistorylength_";
- MaxHistoryLength(): Column<int>(20) {} };
+ struct MaxHistoryLength: Column<std::int64_t> { static constexpr auto name = "maxhistorylength_";
+ MaxHistoryLength(): Column<std::int64_t>(20) {} };
struct RecordHistory: Column<bool> { static constexpr auto name = "recordhistory_";
RecordHistory(): Column<bool>(true) {}};
@@ -86,13 +86,16 @@ class Database
struct Address: Column<std::string> { static constexpr auto name = "address_"; };
+ struct ThrottleLimit: Column<std::int64_t> { static constexpr auto name = "throttlelimit_";
+ ThrottleLimit(): Column<std::int64_t>(10) {} };
+
using MucLogLineTable = Table<Id, Uuid, Owner, IrcChanName, IrcServerName, Date, Body, Nick>;
using MucLogLine = MucLogLineTable::RowType;
using GlobalOptionsTable = Table<Id, Owner, MaxHistoryLength, RecordHistory, GlobalPersistent>;
using GlobalOptions = GlobalOptionsTable::RowType;
- using IrcServerOptionsTable = Table<Id, Owner, Server, Pass, TlsPorts, Ports, Username, Realname, VerifyCert, TrustedFingerprint, EncodingOut, EncodingIn, MaxHistoryLength, Address, Nick>;
+ 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>;
diff --git a/src/database/postgresql_statement.hpp b/src/database/postgresql_statement.hpp
index 37e8ea0..345a942 100644
--- a/src/database/postgresql_statement.hpp
+++ b/src/database/postgresql_statement.hpp
@@ -15,7 +15,7 @@ class PostgresqlStatement: public Statement
body(std::move(body)),
conn(conn)
{}
- ~PostgresqlStatement()
+ virtual ~PostgresqlStatement()
{
PQclear(this->result);
this->result = nullptr;
@@ -89,8 +89,6 @@ class PostgresqlStatement: public Statement
return true;
}
- private:
-
private:
bool execute(const bool second_attempt=false)
{
@@ -119,11 +117,7 @@ private:
PQreset(this->conn);
return this->execute(true);
}
- else
- {
- log_error("Givin up.");
- return false;
- }
+ return false;
}
return true;
}
diff --git a/src/database/query.cpp b/src/database/query.cpp
index d72066e..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)
diff --git a/src/database/query.hpp b/src/database/query.hpp
index ba28b1a..c89371f 100644
--- a/src/database/query.hpp
+++ b/src/database/query.hpp
@@ -12,13 +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);
-template <typename T, typename std::enable_if_t<std::is_integral<T>::value>* = 0>
+void actual_bind(Statement& statement, const OptionalBool& value, int index);
+template <typename T>
void actual_bind(Statement& statement, const T& value, int index)
{
- actual_bind(statement, static_cast<std::int64_t>(value), 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<std::int64_t>(value));
}
-void actual_bind(Statement& statement, const OptionalBool& value, int index);
#ifdef DEBUG_SQL_QUERIES
#include <utils/scopetimer.hpp>
diff --git a/src/database/row.hpp b/src/database/row.hpp
index 1253f93..4004b5d 100644
--- a/src/database/row.hpp
+++ b/src/database/row.hpp
@@ -28,7 +28,7 @@ struct Row
this->clear_col<0>();
}
- std::tuple<T...> columns;
+ std::tuple<T...> columns{};
std::string table_name;
private:
diff --git a/src/database/select_query.hpp b/src/database/select_query.hpp
index b9fdc06..e372f2e 100644
--- a/src/database/select_query.hpp
+++ b/src/database/select_query.hpp
@@ -135,7 +135,7 @@ struct SelectQuery: public Query
};
template <typename... T>
-auto select(const Table<T...> table)
+auto select(const Table<T...>& table)
{
SelectQuery<T...> query(table.name);
return query;
diff --git a/src/irc/irc_client.cpp b/src/irc/irc_client.cpp
index d7fa2cd..de38d42 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,7 +143,14 @@ IrcClient::IrcClient(std::shared_ptr<Poller>& poller, std::string hostname,
bridge(bridge),
welcomed(false),
chanmodes({"", "", "", ""}),
- chantypes({'#', '&'})
+ 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(),
@@ -171,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()
@@ -317,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)
@@ -378,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)
@@ -447,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; });
@@ -466,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;
@@ -489,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;
}
@@ -1123,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);
@@ -1142,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;
@@ -1215,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, "",
@@ -1252,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 fd97fe6..cfb3d21 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
*/
@@ -289,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.
*/
@@ -327,6 +339,10 @@ private:
*/
Bridge& bridge;
/**
+ * Where messaged are stored when they are throttled.
+ */
+ std::deque<std::pair<IrcMessage, MessageCallback>> message_queue{};
+ /**
* The list of joined channels, indexed by name
*/
std::unordered_map<std::string, std::unique_ptr<IrcChannel>> channels;
@@ -385,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 59fda4e..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
@@ -161,7 +129,7 @@ int main(int ac, char** av)
dns_handler.destroy();
#endif
if (identd)
- identd->shutdown();
+ identd->shutdown();
// Cancel the timer for a potential reconnection
TimedEventsManager::instance().cancel("XMPP reconnection");
}
@@ -204,7 +172,7 @@ int main(int ac, char** av)
dns_handler.destroy();
#endif
if (identd)
- identd->shutdown();
+ identd->shutdown();
}
}
// If the only existing connection is the one to the XMPP component:
@@ -223,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/resolver.cpp b/src/network/resolver.cpp
index ae5cecd..d9242e2 100644
--- a/src/network/resolver.cpp
+++ b/src/network/resolver.cpp
@@ -214,6 +214,12 @@ void Resolver::on_hostname6_resolved(dns_rr_a6 *result)
this->call_getaddrinfo(buf, this->port.data(), AI_NUMERICHOST);
}
}
+ else
+ {
+ const auto error = dns_error_messages.find(status);
+ if (error != end(dns_error_messages))
+ this->error_msg = error->second;
+ }
}
void Resolver::after_resolved()
diff --git a/src/network/tcp_client_socket_handler.cpp b/src/network/tcp_client_socket_handler.cpp
index dcf38f9..7d1029f 100644
--- a/src/network/tcp_client_socket_handler.cpp
+++ b/src/network/tcp_client_socket_handler.cpp
@@ -46,15 +46,14 @@ void TCPClientSocketHandler::init_socket(const struct addrinfo* rp)
else
{
utils::ScopeGuard sg([result](){ freeaddrinfo(result); });
- struct addrinfo* rp;
- for (rp = result; rp; rp = rp->ai_next)
+ for (; result; result = result->ai_next)
{
if ((::bind(this->socket,
- reinterpret_cast<const struct sockaddr*>(rp->ai_addr),
- rp->ai_addrlen)) == 0)
+ reinterpret_cast<const struct sockaddr*>(result->ai_addr),
+ result->ai_addrlen)) == 0)
break;
}
- if (!rp)
+ if (!result)
log_error("Failed to bind socket to ", this->bind_addr, ": ",
strerror(errno));
else
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..c13393d 100644
--- a/src/utils/dirname.hpp
+++ b/src/utils/dirname.hpp
@@ -1,6 +1,8 @@
+#pragma once
+
#include <string>
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/get_first_non_empty.cpp b/src/utils/get_first_non_empty.cpp
index 5b3bedb..17585b1 100644
--- a/src/utils/get_first_non_empty.cpp
+++ b/src/utils/get_first_non_empty.cpp
@@ -1,11 +1,8 @@
#include <utils/get_first_non_empty.hpp>
+template <>
bool is_empty(const std::string& val)
{
return val.empty();
}
-bool is_empty(const int& val)
-{
- return val == 0;
-}
diff --git a/src/utils/get_first_non_empty.hpp b/src/utils/get_first_non_empty.hpp
index a38f5fb..1877ee8 100644
--- a/src/utils/get_first_non_empty.hpp
+++ b/src/utils/get_first_non_empty.hpp
@@ -2,8 +2,13 @@
#include <string>
+template <typename T>
+bool is_empty(const T& val)
+{
+ return val == 0;
+}
+template <>
bool is_empty(const std::string& val);
-bool is_empty(const int& val);
template <typename T>
T get_first_non_empty(T&& last)
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/time.cpp b/src/utils/time.cpp
index 71306fd..d848e70 100644
--- a/src/utils/time.cpp
+++ b/src/utils/time.cpp
@@ -1,9 +1,8 @@
#include <utils/time.hpp>
-#include <ctime>
+#include <time.h>
#include <sstream>
#include <iomanip>
-#include <locale>
#include "biboumi.h"
@@ -12,9 +11,10 @@ namespace utils
std::string to_string(const std::chrono::system_clock::time_point::rep& time)
{
constexpr std::size_t stamp_size = 21;
- const std::time_t timestamp = static_cast<std::time_t>(time);
+ const auto timestamp = static_cast<std::time_t>(time);
char date_buf[stamp_size];
- if (std::strftime(date_buf, stamp_size, "%FT%TZ", std::gmtime(&timestamp)) != stamp_size - 1)
+ struct tm tm;
+ if (std::strftime(date_buf, stamp_size, "%FT%TZ", gmtime_r(&timestamp, &tm)) != stamp_size - 1)
return "";
return {std::begin(date_buf), std::end(date_buf) - 1};
}
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/xmpp/adhoc_commands_handler.cpp b/src/xmpp/adhoc_commands_handler.cpp
index bc4c108..ff4c1e5 100644
--- a/src/xmpp/adhoc_commands_handler.cpp
+++ b/src/xmpp/adhoc_commands_handler.cpp
@@ -80,7 +80,10 @@ XmlNode AdhocCommandsHandler::handle_request(const std::string& executor_jid, co
{
command_node["status"] = "executing";
XmlSubNode actions(command_node, "actions");
- XmlSubNode next(actions, "next");
+ if (session.remaining_steps() == 1)
+ XmlSubNode next(actions, "complete");
+ else
+ XmlSubNode next(actions, "next");
}
}
else if (session_it != this->sessions.end() && action == "cancel")
diff --git a/src/xmpp/biboumi_adhoc_commands.cpp b/src/xmpp/biboumi_adhoc_commands.cpp
index 3bd2e5a..113943c 100644
--- a/src/xmpp/biboumi_adhoc_commands.cpp
+++ b/src/xmpp/biboumi_adhoc_commands.cpp
@@ -15,10 +15,17 @@
#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
-#include <ctime>
+# include <time.h>
#endif
using namespace std::string_literals;
@@ -116,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");
@@ -128,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>()));
@@ -140,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");
@@ -156,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");
@@ -184,7 +192,13 @@ void ConfigureGlobalStep2(XmppComponent& xmpp_component, AdhocSession& session,
if (field->get_tag("var") == "max_history_length" &&
value && !value->get_inner().empty())
- options.col<Database::MaxHistoryLength>() = atoi(value->get_inner().data());
+ {
+ try {
+ options.col<Database::MaxHistoryLength>() = std::stol(value->get_inner().data());
+ } catch (const std::logic_error&) {
+ options.col<Database::MaxHistoryLength>() = 20;
+ }
+ }
else if (field->get_tag("var") == "record_history" &&
value && !value->get_inner().empty())
{
@@ -223,6 +237,7 @@ void ConfigureIrcServerStep1(XmppComponent&, AdhocSession& session, XmlNode& com
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");
@@ -236,7 +251,7 @@ void ConfigureIrcServerStep1(XmppComponent&, AdhocSession& session, XmlNode& com
field["var"] = "address";
field["type"] = "text-single";
field["label"] = "Address";
- field["desc"] = "The address (hostname or IP) to connect to.";
+ 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);
@@ -249,7 +264,7 @@ void ConfigureIrcServerStep1(XmppComponent&, AdhocSession& session, XmlNode& com
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");
@@ -263,7 +278,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");
@@ -276,7 +291,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");
@@ -302,7 +317,7 @@ void ConfigureIrcServerStep1(XmppComponent&, AdhocSession& session, XmlNode& com
field["var"] = "nick";
field["type"] = "text-single";
field["label"] = "Nickname";
- field["desc"] = "If set, will override the nickname provided in the initial presence sent to join the first server channel";
+ 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");
@@ -315,7 +330,7 @@ void ConfigureIrcServerStep1(XmppComponent&, AdhocSession& session, XmlNode& com
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");
@@ -327,7 +342,7 @@ void ConfigureIrcServerStep1(XmppComponent&, AdhocSession& session, XmlNode& com
XmlSubNode after_cnt_cmd(x, "field");
after_cnt_cmd["var"] = "after_connect_commands";
after_cnt_cmd["type"] = "text-multi";
- after_cnt_cmd["desc"] = "Custom IRC commands sent after the connection is established with the server.";
+ 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)
{
@@ -364,10 +379,28 @@ 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 max_history_length(x, "field");
+ max_history_length["var"] = "max_history_length";
+ max_history_length["type"] = "text-single";
+ max_history_length["label"] = "Throttle limit";
+ XmlSubNode value(max_history_length, "value");
+ value.set_inner(std::to_string(options.col<Database::MaxHistoryLength>()));
+ }
+
+ {
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())
{
@@ -380,7 +413,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())
{
@@ -390,8 +423,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)
{
@@ -472,6 +507,31 @@ 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>() = 10;
+ }
+ 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") == "max_history_length" && value)
+ {
+ try {
+ options.col<Database::MaxHistoryLength>() = std::stol(value->get_inner());
+ } catch (const std::logic_error&) {
+ options.col<Database::MaxHistoryLength>() = 20;
+ }
+ }
+
else if (field->get_tag("var") == "encoding_out" && value)
options.col<Database::EncodingOut>() = value->get_inner();
@@ -509,6 +569,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");
@@ -521,7 +582,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");
@@ -541,7 +602,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())
{
@@ -554,7 +615,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())
{
@@ -567,7 +628,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");
@@ -847,12 +908,13 @@ void GetIrcConnectionInfoStep1(XmppComponent& component, AdhocSession& session,
if (irc->is_using_tls())
ss << " (using TLS)";
const std::time_t now_c = std::chrono::system_clock::to_time_t(irc->connection_date);
+ struct tm tm;
#ifdef HAS_PUT_TIME
- ss << " since " << std::put_time(std::localtime(&now_c), "%F %T");
+ ss << " since " << std::put_time(localtime_r(&now_c, &tm), "%F %T");
#else
constexpr std::size_t timestamp_size{10 + 1 + 8 + 1};
char buf[timestamp_size] = {};
- const auto res = std::strftime(buf, timestamp_size, "%F %T", std::localtime(&now_c));
+ const auto res = std::strftime(buf, timestamp_size, "%F %T", localtime(&now_c, &tm));
if (res > 0)
ss << " since " << buf;
#endif
diff --git a/src/xmpp/biboumi_component.cpp b/src/xmpp/biboumi_component.cpp
index be34873..6fe6972 100644
--- a/src/xmpp/biboumi_component.cpp
+++ b/src/xmpp/biboumi_component.cpp
@@ -102,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)
@@ -185,8 +185,13 @@ void BiboumiComponent::handle_presence(const Stanza& stanza)
}
bridge->join_irc_channel(iid, to.resource, password ? password->get_inner(): "",
from.resource, history_limit, x != nullptr);
- if (!own_nick.empty() && own_nick != to.resource)
- bridge->send_irc_nick_change(iid, to.resource, from.resource);
+ const IrcClient* irc = bridge->find_irc_client(iid.get_server());
+ if (irc)
+ {
+ const auto chan = irc->find_channel(iid.get_local());
+ if (chan->joined)
+ bridge->send_irc_nick_change(iid, to.resource, from.resource);
+ }
}
else if (type == "unavailable")
{
@@ -273,9 +278,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);
@@ -284,7 +290,15 @@ void BiboumiComponent::handle_message(const Stanza& stanza)
{
if (body && !body->get_inner().empty())
{
- bridge->send_channel_message(iid, body->get_inner(), id);
+ 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)
@@ -350,7 +364,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)
{
@@ -514,7 +527,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)
@@ -592,7 +609,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;
@@ -751,7 +767,7 @@ bool BiboumiComponent::handle_mam_request(const Stanza& stanza)
if (limit < 0 || limit > 100)
limit = 100;
auto result = Database::get_muc_logs(from.bare(), iid.get_local(), iid.get_server(),
- limit,
+ static_cast<std::size_t>(limit),
start, end,
reference_record_id, paging_order);
bool complete = std::get<bool>(result);
@@ -964,7 +980,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, {});
@@ -980,11 +997,31 @@ void BiboumiComponent::send_irc_channel_disco_info(const std::string& id, const
identity["category"] = "conference";
identity["type"] = "irc";
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})
+ for (const char *ns: {DISCO_INFO_NS, MUC_NS, ADHOC_NS, PING_NS, MAM_NS, VERSION_NS, STABLE_MUC_ID_NS, SELF_PING_FLAG, "muc_nonanonymous"})
{
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 b3d925e..f82f9ce 100644
--- a/src/xmpp/xmpp_component.cpp
+++ b/src/xmpp/xmpp_component.cpp
@@ -298,8 +298,8 @@ void XmppComponent::send_message(const std::string& from, Xmpp::body&& body, con
{
XmlSubNode private_node(message, "private");
private_node["xmlns"] = "urn:xmpp:carbons:2";
- XmlSubNode nocopy(message, "no-copy");
- nocopy["xmlns"] = "urn:xmpp:hints";
+ XmlSubNode nocopy_node(message, "no-copy");
+ nocopy_node["xmlns"] = "urn:xmpp:hints";
}
if (muc_private)
{
@@ -340,8 +340,12 @@ void XmppComponent::send_user_join(const std::string& from,
if (self)
{
- XmlSubNode status(x, "status");
- status["code"] = "110";
+ XmlSubNode status_self(x, "status");
+ status_self["code"] = "110";
+ XmlSubNode status_nick_modified(x, "status");
+ status_nick_modified["code"] = "210";
+ XmlSubNode status_nonanonymous(x, "status");
+ status_nonanonymous["code"] = "100";
}
}
this->send_stanza(presence);
@@ -588,8 +592,8 @@ void XmppComponent::send_version(const std::string& id, const std::string& jid_t
name.set_inner("biboumi");
}
{
- XmlSubNode version(query, "version");
- version.set_inner(SOFTWARE_VERSION);
+ XmlSubNode version_node(query, "version");
+ version_node.set_inner(SOFTWARE_VERSION);
}
{
XmlSubNode os(query, "os");
diff --git a/src/xmpp/xmpp_component.hpp b/src/xmpp/xmpp_component.hpp
index e18da40..156e286 100644
--- a/src/xmpp/xmpp_component.hpp
+++ b/src/xmpp/xmpp_component.hpp
@@ -38,6 +38,7 @@
#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"
+#define SELF_PING_FLAG MUC_NS"#self-ping-optimization"
/**
* An XMPP component, communicating with an XMPP server using the protocole
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/end_to_end/__main__.py b/tests/end_to_end/__main__.py
index a029bea..cef554e 100644
--- a/tests/end_to_end/__main__.py
+++ b/tests/end_to_end/__main__.py
@@ -1,18 +1,19 @@
#!/usr/bin/env python3
+from functions import StanzaError, SkipStepError
+
import collections
-import lxml.etree
+import importlib
+import sequences
import datetime
import slixmpp
import asyncio
import logging
import signal
import atexit
-import time
import sys
-import io
import os
-from functools import partial
+
from slixmpp.xmlstream.matcher.base import MatcherBase
if not hasattr(asyncio, "ensure_future"):
@@ -25,20 +26,35 @@ class MatchAll(MatcherBase):
return True
-class StanzaError(Exception):
- """
- Raised when a step fails.
+class Scenario:
+ """Defines a list of actions that are executed in sequence, until one of
+ them throws an exception, or until the end. An action can be something
+ like “send a stanza”, “receive the next stanza and check that it matches
+ the given XPath”, “send a signal”, “wait for the end of the process”,
+ etc
"""
- pass
+ def __init__(self, name, steps, conf):
+ """
+ Steps is a list of 2-tuple:
+ [(action, answer), (action, answer)]
+ """
+ self.name = name
+ self.steps = []
+ self.conf = conf
-class SkipStepError(Exception):
- """
- Raised by a step when it needs to be skiped, by running
- the next available step immediately.
- """
- pass
+ def unwrap_tuples(elements):
+ """Yields all the value contained in the tuples, of tuples, of tuples…
+ For example unwrap_tuples((1, 2, 3, (4, 5, (6,)))) will yield 1, 2, 3, 4, 5, 6
+ This works with any depth"""
+ if isinstance(elements, collections.abc.Iterable):
+ for elem in elements:
+ yield from unwrap_tuples(elem)
+ else:
+ yield elements
+ for step in unwrap_tuples(steps):
+ self.steps.append(step)
class XMPPComponent(slixmpp.BaseXMPP):
"""
@@ -54,7 +70,7 @@ class XMPPComponent(slixmpp.BaseXMPP):
self.stream_header = '<stream:stream %s %s from="%s" id="%s">' % (
'xmlns="jabber:component:accept"',
'xmlns:stream="%s"' % self.stream_ns,
- self.boundjid, self.get_id())
+ self.boundjid, self.new_id())
self.stream_footer = "</stream:stream>"
self.register_handler(slixmpp.Callback('Match All',
@@ -67,6 +83,7 @@ class XMPPComponent(slixmpp.BaseXMPP):
self.scenario = scenario
self.biboumi = biboumi
+ self.timeout_handler = None
# A callable, taking a stanza as argument and raising a StanzaError
# exception if the test should fail.
self.stanza_checker = None
@@ -80,6 +97,13 @@ class XMPPComponent(slixmpp.BaseXMPP):
self.scenario.steps = []
self.failed = True
+ def on_timeout(self, xpaths):
+ error_msg = "Timeout while waiting for a stanza that would match the expected xpath(s):"
+ for xpath in xpaths:
+ error_msg += "\n" + xpath
+ self.error(error_msg)
+ self.run_scenario()
+
def on_end_session(self, _):
self.loop.stop()
@@ -97,10 +121,13 @@ class XMPPComponent(slixmpp.BaseXMPP):
self.run_scenario()
def run_scenario(self):
+ if self.timeout_handler is not None:
+ self.timeout_handler.cancel()
+ self.timeout_handler = None
if self.scenario.steps:
step = self.scenario.steps.pop(0)
try:
- step(self, self.biboumi)
+ step(xmpp=self, biboumi=self.biboumi)
except Exception as e:
self.error(e)
self.run_scenario()
@@ -113,106 +140,6 @@ class XMPPComponent(slixmpp.BaseXMPP):
self.accepting_server = yield from self.loop.create_server(lambda: self,
"127.0.0.1", 8811, reuse_address=True)
- def check_stanza_against_all_expected_xpaths(self):
- pass
-
-
-def match(stanza, xpath):
- tree = lxml.etree.parse(io.StringIO(str(stanza)))
- matched = tree.xpath(xpath, namespaces={'re': 'http://exslt.org/regular-expressions',
- 'muc_user': 'http://jabber.org/protocol/muc#user',
- 'muc_owner': 'http://jabber.org/protocol/muc#owner',
- 'muc': 'http://jabber.org/protocol/muc',
- 'disco_info': 'http://jabber.org/protocol/disco#info',
- 'muc_traffic': 'http://jabber.org/protocol/muc#traffic',
- 'disco_items': 'http://jabber.org/protocol/disco#items',
- 'commands': 'http://jabber.org/protocol/commands',
- 'dataform': 'jabber:x:data',
- 'version': 'jabber:iq:version',
- 'mam': 'urn:xmpp:mam:2',
- 'rms': 'http://jabber.org/protocol/rsm',
- 'delay': 'urn:xmpp:delay',
- 'forward': 'urn:xmpp:forward:0',
- 'client': 'jabber:client',
- 'rsm': 'http://jabber.org/protocol/rsm',
- 'carbon': 'urn:xmpp:carbons:2',
- 'hints': 'urn:xmpp:hints',
- 'stanza': 'urn:ietf:params:xml:ns:xmpp-stanzas',
- 'stable_id': 'urn:xmpp:sid:0'})
- return matched
-
-
-def check_xpath(xpaths, xmpp, after, stanza):
- for xpath in xpaths:
- expected = True
- real_xpath = xpath
- # We can check that a stanza DOESN’T match, by adding a ! before it.
- if xpath.startswith('!'):
- expected = False
- xpath = xpath[1:]
- matched = match(stanza, xpath)
- if (expected and not matched) or (not expected and matched):
- 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:
- af(stanza, xmpp)
- else:
- after(stanza, xmpp)
-
-def all_xpaths_match(stanza, xpaths):
- for xpath in xpaths:
- matched = match(stanza, xpath)
- if not matched:
- return False
- return True
-
-def check_list_of_xpath(list_of_xpaths, xmpp, stanza):
- found = None
- for i, xpaths in enumerate(list_of_xpaths):
- if all_xpaths_match(stanza, xpaths):
- found = True
- list_of_xpaths.pop(i)
- break
-
- if not found:
- raise StanzaError("Received stanza “%s” did not match any of the expected xpaths:\n%s" % (stanza, list_of_xpaths))
-
- if list_of_xpaths:
- step = partial(expect_unordered_already_formatted, list_of_xpaths)
- xmpp.scenario.steps.insert(0, step)
-
-
-def check_xpath_optional(xpaths, xmpp, after, stanza):
- try:
- check_xpath(xpaths, xmpp, after, stanza)
- except StanzaError:
- raise SkipStepError()
-
-
-class Scenario:
- """Defines a list of actions that are executed in sequence, until one of
- them throws an exception, or until the end. An action can be something
- like “send a stanza”, “receive the next stanza and check that it matches
- the given XPath”, “send a signal”, “wait for the end of the process”,
- etc
- """
-
- def __init__(self, name, steps, conf="basic"):
- """
- Steps is a list of 2-tuple:
- [(action, answer), (action, answer)]
- """
- self.name = name
- self.steps = []
- self.conf = conf
- for elem in steps:
- if isinstance(elem, collections.Iterable):
- for step in elem:
- self.steps.append(step)
- else:
- self.steps.append(elem)
-
class ProcessRunner:
def __init__(self):
@@ -261,48 +188,6 @@ class IrcServerRunner(ProcessRunner):
self.create = asyncio.create_subprocess_exec("charybdis", "-foreground", "-configfile", os.getcwd() + "/../tests/end_to_end/ircd.conf",
stderr=asyncio.subprocess.PIPE)
-
-def send_stanza(stanza, xmpp, biboumi):
- replacements = common_replacements
- replacements.update(xmpp.saved_values)
- xmpp.send_raw(stanza.format_map(replacements))
- asyncio.get_event_loop().call_soon(xmpp.run_scenario)
-
-
-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(replacements)], xmpp, after)
- elif isinstance(xpaths, tuple):
- 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)))
-
-def save_current_timestamp_plus_delta(key, delta, message, xmpp):
- now_plus_delta = datetime.datetime.utcnow() + delta
- xmpp.saved_values[key] = now_plus_delta.strftime("%FT%T.967Z")
-
-def sleep_for(duration, xmpp, biboumi):
- time.sleep(duration)
- asyncio.get_event_loop().call_soon(xmpp.run_scenario)
-
-# list_of_xpaths: [(xpath, xpath), (xpath, xpath), (xpath)]
-def expect_unordered(list_of_xpaths, xmpp, biboumi):
- formatted_list_of_xpaths = []
- for xpaths in list_of_xpaths:
- formatted_xpaths = []
- for xpath in xpaths:
- formatted_xpath = xpath.format_map(common_replacements)
- formatted_xpaths.append(formatted_xpath)
- formatted_list_of_xpaths.append(tuple(formatted_xpaths))
- expect_unordered_already_formatted(formatted_list_of_xpaths, xmpp, biboumi)
-
-def expect_unordered_already_formatted(formatted_list_of_xpaths, xmpp, biboumi):
- xmpp.stanza_checker = partial(check_list_of_xpath, formatted_list_of_xpaths, xmpp)
-
-
class BiboumiTest:
"""
Spawns a biboumi process and a fake XMPP Component that will run a
@@ -333,6 +218,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)
@@ -344,13 +231,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
@@ -390,2852 +280,42 @@ port=8811
persistent_by_default=true
""",}
-common_replacements = {
- 'irc_server_one': 'irc.localhost@biboumi.localhost',
- 'irc_server_two': 'localhost@biboumi.localhost',
- 'irc_host_one': 'irc.localhost',
- 'irc_host_two': 'localhost',
- 'biboumi_host': 'biboumi.localhost',
- 'resource_one': 'resource1',
- 'resource_two': 'resource2',
- 'nick_one': 'Nick',
- 'jid_one': 'first@example.com',
- 'jid_two': 'second@example.com',
- 'jid_admin': 'admin@example.com',
- 'nick_two': 'Bobby',
- 'nick_three': 'Bernard',
- 'lower_nick_one': 'nick',
- 'lower_nick_two': 'bobby',
-}
-
-
-def handshake_sequence():
- return (partial(expect_stanza, "//handshake"),
- partial(send_stanza, "<handshake xmlns='jabber:component:accept'/>"))
-
-
-def connection_begin_sequence(irc_host, jid, expected_irc_presence=False, fixed_irc_server=False):
- jid = jid.format_map(common_replacements)
- if fixed_irc_server:
- xpath = "/message[@to='" + jid + "'][@from='biboumi.localhost']/body[text()='%s']"
- xpath_re = "/message[@to='" + jid + "'][@from='biboumi.localhost']/body[re:test(text(), '%s')]"
- else:
- xpath = "/message[@to='" + jid + "'][@from='" + irc_host + "@biboumi.localhost']/body[text()='%s']"
- xpath_re = "/message[@to='" + jid + "'][@from='" + irc_host + "@biboumi.localhost']/body[re:test(text(), '%s')]"
- result = (
- partial(expect_stanza,
- (xpath % ('Connecting to %s:6697 (encrypted)' % irc_host),
- "/message/hints:no-copy",
- "/message/carbon:private"
- )
- ),
- partial(expect_stanza,
- xpath % 'Connection failed: Connection refused'),
- partial(expect_stanza,
- xpath % ('Connecting to %s:6670 (encrypted)' % irc_host)),
- partial(expect_stanza,
- xpath % 'Connection failed: Connection refused'),
- partial(expect_stanza,
- xpath % ('Connecting to %s:6667 (not encrypted)' % irc_host)),
- partial(expect_stanza,
- xpath % 'Connected to IRC server.'))
-
- if expected_irc_presence:
- result += (partial(expect_stanza, "/presence[@from='" + irc_host + "@biboumi.localhost']"),)
-
- # These five messages can be receive in any order
- result += (
- partial(expect_stanza,
- xpath_re % (r'^%s: (\*\*\* Checking Ident|\*\*\* Looking up your hostname\.\.\.|\*\*\* Found your hostname: .*|ACK multi-prefix|\*\*\* Got Ident response)$' % 'irc.localhost')),
- partial(expect_stanza,
- xpath_re % (r'^%s: (\*\*\* Checking Ident|\*\*\* Looking up your hostname\.\.\.|\*\*\* Found your hostname: .*|ACK multi-prefix|\*\*\* Got Ident response)$' % 'irc.localhost')),
- partial(expect_stanza,
- xpath_re % (r'^%s: (\*\*\* Checking Ident|\*\*\* Looking up your hostname\.\.\.|\*\*\* Found your hostname: .*|ACK multi-prefix|\*\*\* Got Ident response)$' % 'irc.localhost')),
- partial(expect_stanza,
- xpath_re % (r'^%s: (\*\*\* Checking Ident|\*\*\* Looking up your hostname\.\.\.|\*\*\* Found your hostname: .*|ACK multi-prefix|\*\*\* Got Ident response)$' % 'irc.localhost')),
- partial(expect_stanza,
- xpath_re % (r'^%s: (\*\*\* Checking Ident|\*\*\* Looking up your hostname\.\.\.|\*\*\* Found your hostname: .*|ACK multi-prefix|\*\*\* Got Ident response)$' % 'irc.localhost')),
- )
-
- return result
-
-def connection_tls_begin_sequence(irc_host, jid, fixed_irc_server):
- jid = jid.format_map(common_replacements)
- if fixed_irc_server:
- xpath = "/message[@to='" + jid + "'][@from='biboumi.localhost']/body[text()='%s']"
- xpath_re = "/message[@to='" + jid + "'][@from='biboumi.localhost']/body[re:test(text(), '%s')]"
- else:
- xpath = "/message[@to='" + jid + "'][@from='" + irc_host + "@biboumi.localhost']/body[text()='%s']"
- xpath_re = "/message[@to='" + jid + "'][@from='" + irc_host + "@biboumi.localhost']/body[re:test(text(), '%s')]"
- irc_host = 'irc.localhost'
- return (
- partial(expect_stanza,
- (xpath % ('Connecting to %s:7778 (encrypted)' % irc_host),
- "/message/hints:no-copy",
- "/message/carbon:private",
- )
- ),
- partial(expect_stanza,
- xpath % 'Connected to IRC server (encrypted).'),
- # These five messages can be receive in any order
- partial(expect_stanza,
- xpath_re % (r'^%s: (\*\*\* Checking Ident|\*\*\* Looking up your hostname\.\.\.|\*\*\* Found your hostname: .*|ACK multi-prefix|\*\*\* Got Ident response)$' % irc_host)),
- partial(expect_stanza,
- xpath_re % (r'^%s: (\*\*\* Checking Ident|\*\*\* Looking up your hostname\.\.\.|\*\*\* Found your hostname: .*|ACK multi-prefix|\*\*\* Got Ident response)$' % irc_host)),
- partial(expect_stanza,
- xpath_re % (r'^%s: (\*\*\* Checking Ident|\*\*\* Looking up your hostname\.\.\.|\*\*\* Found your hostname: .*|ACK multi-prefix|\*\*\* Got Ident response)$' % irc_host)),
- partial(expect_stanza,
- xpath_re % (r'^%s: (\*\*\* Checking Ident|\*\*\* Looking up your hostname\.\.\.|\*\*\* Found your hostname: .*|ACK multi-prefix|\*\*\* Got Ident response)$' % irc_host)),
- partial(expect_stanza,
- xpath_re % (r'^%s: (\*\*\* Checking Ident|\*\*\* Looking up your hostname\.\.\.|\*\*\* Found your hostname: .*|ACK multi-prefix|\*\*\* Got Ident response)$' % irc_host)),
- )
-
-def connection_end_sequence(irc_host, jid, fixed_irc_server=False):
- jid = jid.format_map(common_replacements)
- if fixed_irc_server:
- xpath = "/message[@to='" + jid + "'][@from='biboumi.localhost']/body[text()='%s']"
- xpath_re = "/message[@to='" + jid + "'][@from='biboumi.localhost']/body[re:test(text(), '%s')]"
- else:
- xpath = "/message[@to='" + jid + "'][@from='" + irc_host + "@biboumi.localhost']/body[text()='%s']"
- xpath_re = "/message[@to='" + jid + "'][@from='" + irc_host + "@biboumi.localhost']/body[re:test(text(), '%s')]"
- irc_host = 'irc.localhost'
- return (
- partial(expect_stanza,
- xpath_re % (r'^%s: Your host is .*$' % irc_host)),
- partial(expect_stanza,
- xpath_re % (r'^%s: This server was created .*$' % irc_host)),
- partial(expect_stanza,
- xpath_re % (r'^%s: There are \d+ users and \d+ invisible on \d+ servers$' % irc_host)),
- partial(expect_stanza,
- xpath_re % (r'^%s: \d+ unknown connection\(s\)$' % irc_host), optional=True),
- partial(expect_stanza,
- xpath_re % (r'^%s: \d+ channels formed$' % irc_host), optional=True),
- partial(expect_stanza,
- xpath_re % (r'^%s: I have \d+ clients and \d+ servers$' % irc_host)),
- partial(expect_stanza,
- xpath_re % (r'^%s: \d+ \d+ Current local users \d+, max \d+$' % irc_host)),
- partial(expect_stanza,
- xpath_re % (r'^%s: \d+ \d+ Current global users \d+, max \d+$' % irc_host)),
- partial(expect_stanza,
- xpath_re % (r'^%s: Highest connection count: \d+ \(\d+ clients\) \(\d+ connections received\)$' % irc_host)),
- partial(expect_stanza,
- xpath % "- This is charybdis MOTD you might replace it, but if not your friends will\n- laugh at you.\n"),
- partial(expect_stanza,
- xpath_re % r'^User mode for \w+ is \[\+Z?i\]$'),
- )
-
-def connection_middle_sequence(irc_host, jid, fixed_irc_server=False):
- if fixed_irc_server:
- xpath_re = "/message[@to='" + jid + "'][@from='biboumi.localhost']/body[re:test(text(), '%s')]"
- else:
- xpath_re = "/message[@to='" + jid + "'][@from='" + irc_host + "@biboumi.localhost']/body[re:test(text(), '%s')]"
- irc_host = 'irc.localhost'
- return (
- partial(expect_stanza, xpath_re % (r'^%s: \*\*\* You are exempt from flood limits$' % irc_host)),
- )
-
-
-def connection_sequence(irc_host, jid, expected_irc_presence=False, fixed_irc_server=False):
- return connection_begin_sequence(irc_host, jid, expected_irc_presence, fixed_irc_server=fixed_irc_server) +\
- connection_middle_sequence(irc_host, jid, fixed_irc_server=fixed_irc_server) +\
- connection_end_sequence(irc_host, jid, fixed_irc_server=fixed_irc_server)
-
-def connection_tls_sequence(irc_host, jid, fixed_irc_server=False):
- return connection_tls_begin_sequence(irc_host, jid, fixed_irc_server) + \
- connection_middle_sequence(irc_host, jid, fixed_irc_server) +\
- connection_end_sequence(irc_host, jid, fixed_irc_server)
-
-
-def extract_attribute(xpath, name, stanza):
- matched = match(stanza, xpath)
- return matched[0].get(name)
-
-def chan_name_from_jid(jid):
- return jid[1:jid.find('%')]
-
-def extract_text(xpath, stanza):
- matched = match(stanza, xpath)
- return matched[0].text
-
-def save_value(name, func, stanza, xmpp):
- xmpp.saved_values[name] = func(stanza)
+def get_scenarios(test_path, provided_scenar_names):
+ """
+ :param test_path: The path containing all the tests
+ :param provided_scenar_names: a list of scenario names provided on the
+ command line by the user. May be empty
+ :return: The list of scenarios to be run. If provided_scenar_names is
+ empty, we return all the existing scenarios, otherwise we just return
+ the one from that list
+ """
+ scenarios = []
+ for entry in os.scandir(os.path.join(test_path, "scenarios")):
+ if entry.is_file() and not entry.name.startswith('.') and entry.name.endswith('.py'):
+ module_name = entry.name[:-3]
+ if provided_scenar_names and module_name not in provided_scenar_names:
+ continue
+ if module_name == "__init__" or (provided_scenar_names and module_name not in provided_scenar_names):
+ continue
+ module_full_path = "scenarios.{}".format(module_name)
+ mod = importlib.import_module(module_full_path)
+ conf = "basic"
+ if hasattr(mod, "conf"):
+ conf = mod.conf
+ # Every scenario needs to start with the handshake sequence.
+ # Instead of repeating it everytime, we add it implicitely. This
+ # is done here.
+ scenarios.append(Scenario(module_name, (sequences.handshake(),) + mod.scenario, conf))
+ return scenarios
if __name__ == '__main__':
-
atexit.register(asyncio.get_event_loop().close)
- # Start the test component, accepting connections on the configured
- # port.
- scenarios = (
- Scenario("basic_handshake_success",
- [
- handshake_sequence()
- ]),
- Scenario("irc_server_connection",
- [
- 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}'),
- ]),
- Scenario("irc_server_connection_failure",
- [
- handshake_sequence(),
- partial(send_stanza,
- "<presence from='{jid_one}/{resource_one}' to='#foo%doesnotexist@{biboumi_host}/{nick_one}' />"),
- partial(expect_stanza,
- "/message/body[text()='Connecting to doesnotexist:6697 (encrypted)']"),
- partial(expect_stanza,
- "/message/body[re:test(text(), 'Connection failed: (Domain name not found|Name or service not known)')]"),
- partial(expect_stanza,
- ("/presence[@from='#foo%doesnotexist@{biboumi_host}/{nick_one}']/muc:x",
- "/presence/error[@type='cancel']/stanza:item-not-found",
- "/presence/error[@type='cancel']/stanza:text[re:test(text(), '(Domain name not found|Name or service not known)')]")),
- ]),
- Scenario("simple_channel_join",
- [
- 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[@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())]"),
- ]),
- 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(),
- 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 raw QUIT message
- partial(send_stanza, "<message from='{jid_one}/{resource_one}' to='{irc_server_one}' type='chat'><body>QUIT bye bye</body></message>"),
- partial(expect_stanza, ("/presence[@from='#foo%{irc_server_one}/{nick_one}'][@type='unavailable']/muc_user:x/muc_user:status[@code='110']",
- "/presence[@from='#foo%{irc_server_one}/{nick_one}'][@type='unavailable']/muc_user:x/muc_user:status[@code='110']",)),
- ]),
- Scenario("multiple_channels_join",
- [
- handshake_sequence(),
- partial(send_stanza,
- "<presence from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}/{nick_one}' />"),
- partial(send_stanza,
- "<presence from='{jid_one}/{resource_one}' to='#bar%{irc_server_one}/{nick_one}' />"),
- partial(send_stanza,
- "<presence from='{jid_one}/{resource_one}' to='#baz%{irc_server_one}/{nick_one}'> <x xmlns='http://jabber.org/protocol/muc'><password>SECRET</password></x></presence>"),
-
- 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(expect_stanza,
- "/message/body[text()='Mode #bar [+nt] by {irc_host_one}']"),
- partial(expect_stanza,
- ("/presence[@to='{jid_one}/{resource_one}'][@from='#bar%{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='#bar%{irc_server_one}'][@type='groupchat']/subject[not(text())]"),
-
- partial(expect_stanza,
- "/message/body[text()='Mode #baz [+nt] by {irc_host_one}']"),
- partial(expect_stanza,
- ("/presence[@to='{jid_one}/{resource_one}'][@from='#baz%{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='#baz%{irc_server_one}'][@type='groupchat']/subject[not(text())]"),
- ]),
- Scenario("not_connected_error",
- [
- handshake_sequence(),
- partial(send_stanza,
- "<presence type='unavailable' from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}/{nick_one}' />"),
- 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())]"),
- ]),
- Scenario("channel_join_with_two_users",
- [
- handshake_sequence(),
- # First user joins
- partial(send_stanza,
- "<presence from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}/{nick_one}' />"),
- connection_sequence("irc.localhost", '{jid_one}/{resource_one}'),
- partial(expect_stanza,
- "/message/body[text()='Mode #foo [+nt] by {irc_host_one}']"),
- partial(expect_stanza,
- ("/presence[@to='{jid_one}/{resource_one}'][@from='#foo%{irc_server_one}/{nick_one}']/muc_user:x/muc_user:item[@affiliation='admin'][@jid='~nick@localhost'][@role='moderator']",
- "/presence/muc_user:x/muc_user:status[@code='110']")
- ),
- partial(expect_stanza, "/message[@from='#foo%{irc_server_one}'][@type='groupchat']/subject[not(text())]"),
-
- # 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[@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())]",),
- ]),
- ]),
- 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}'><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}']"),
- partial(expect_stanza,
- ("/presence[@to='{jid_one}/{resource_one}'][@from='#foo%{irc_server_one}/{nick_one}']/muc_user:x/muc_user:item[@affiliation='admin'][@jid='~nick@localhost'][@role='moderator']",
- "/presence/muc_user:x/muc_user:status[@code='110']")
- ),
- partial(expect_stanza, "/message[@from='#foo%{irc_server_one}'][@type='groupchat']/subject[not(text())]"),
-
- # Second user joins
- partial(send_stanza,
- "<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']",),
- ("/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())]",),
- ]),
-
- # 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",
- [
- handshake_sequence(),
- # First user joins
- partial(send_stanza,
- "<presence from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}/{nick_one}' />"),
- connection_sequence("irc.localhost", '{jid_one}/{resource_one}'),
- partial(expect_stanza,
- "/message/body[text()='Mode #foo [+nt] by {irc_host_one}']"),
- partial(expect_stanza,
- ("/presence[@to='{jid_one}/{resource_one}'][@from='#foo%{irc_server_one}/{nick_one}']/muc_user:x/muc_user:item[@affiliation='admin'][@jid='~nick@localhost'][@role='moderator']",
- "/presence/muc_user:x/muc_user:status[@code='110']")
- ),
- partial(expect_stanza, "/message[@from='#foo%{irc_server_one}'][@type='groupchat']/subject[not(text())]"),
-
- # Set a password in the room, by using /mode +k
- partial(send_stanza, "<message from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}' type='groupchat'><body>/mode +k SECRET</body></message>"),
- partial(expect_stanza, "/message[@from='#foo%{irc_server_one}'][@to='{jid_one}/{resource_one}'][@type='groupchat']/body[text()='Mode #foo [+k SECRET] by {nick_one}']"),
-
- # Second user tries to join, without a password
- 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_stanza, "/message/body[text()='{irc_host_one}: #foo: Cannot join channel (+k) - bad key']"),
- partial(expect_stanza,
- "/presence[@type='error'][@from='#foo%{irc_server_one}/{nick_two}']/error[@type='auth']/stanza:not-authorized",
- ),
-
- # Second user joins, with a password
- partial(send_stanza,
- "<presence from='{jid_two}/{resource_one}' to='#foo%{irc_server_one}/{nick_two}'> <x xmlns='http://jabber.org/protocol/muc'><password>SECRET</password></x></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']",),
- ("/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())]",),
- ]),
-
- ]),
- Scenario("channel_custom_topic",
- [
- handshake_sequence(),
- # First user joins
- partial(send_stanza,
- "<presence from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}/{nick_one}' />"),
- connection_sequence("irc.localhost", '{jid_one}/{resource_one}'),
- partial(expect_stanza,
- "/message/body[text()='Mode #foo [+nt] by {irc_host_one}']"),
- partial(expect_stanza,
- ("/presence[@to='{jid_one}/{resource_one}'][@from='#foo%{irc_server_one}/{nick_one}']/muc_user:x/muc_user:item[@affiliation='admin'][@jid='~nick@localhost'][@role='moderator']",
- "/presence/muc_user:x/muc_user:status[@code='110']")
- ),
- partial(expect_stanza, "/message[@from='#foo%{irc_server_one}'][@type='groupchat']/subject[not(text())]"),
-
- # First user sets the topic
- partial(send_stanza,
- "<message from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}' type='groupchat'><subject>TOPIC TEST</subject></message>"),
- partial(expect_stanza, "/message[@from='#foo%{irc_server_one}/{nick_one}'][@type='groupchat'][@to='{jid_one}/{resource_one}']/subject[text()='TOPIC TEST']"),
-
- # 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_stanza,
- ("/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']",)),
- # The other user presence
- partial(expect_stanza,
- "/presence[@to='{jid_two}/{resource_one}'][@from='#foo%{irc_server_one}/{nick_one}']/muc_user:x/muc_user:item[@affiliation='admin'][@role='moderator']"),
- # Our own presence
- partial(expect_stanza,
- ("/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']")
- ),
- partial(expect_stanza, "/message[@from='#foo%{irc_server_one}/{nick_one}'][@type='groupchat']/subject[text()='TOPIC TEST']"),
- ]),
- Scenario("multiline_topic",
- [
- handshake_sequence(),
- # 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"),
- partial(expect_stanza, "/presence"),
- partial(expect_stanza, "/message[@from='#foo%{irc_server_one}'][@type='groupchat']/subject[not(text())]"),
-
- # User tries to set a multiline topic
- partial(send_stanza,
- "<message from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}' type='groupchat'><subject>FIRST LINE\nSECOND LINE.</subject></message>"),
- partial(expect_stanza, "/message[@from='#foo%{irc_server_one}/{nick_one}'][@type='groupchat'][@to='{jid_one}/{resource_one}']/subject[text()='FIRST LINE SECOND LINE.']"),
- ]),
- Scenario("channel_basic_join_on_fixed_irc_server",
- [
- handshake_sequence(),
- partial(send_stanza,
- "<presence from='{jid_one}/{resource_one}' to='#zgeg@{biboumi_host}/{nick_one}' />"),
- connection_sequence("irc.localhost", '{jid_one}/{resource_one}', fixed_irc_server=True),
- partial(expect_stanza,
- "/message/body[text()='Mode #zgeg [+nt] by {irc_host_one}']"),
- partial(expect_stanza,
- ("/presence[@to='{jid_one}/{resource_one}'][@from='#zgeg@{biboumi_host}/{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='#zgeg@{biboumi_host}'][@type='groupchat']/subject[not(text())]"),
- ], conf='fixed_server'
- ),
- Scenario("list_adhoc",
- [
- 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[@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[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[@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[8]",
- "!/iq/disco_items:query/disco_items:item[9]")),
- ], conf='fixed_server'),
- Scenario("list_adhoc_irc",
- [
- handshake_sequence(),
- partial(send_stanza, "<iq type='get' id='idwhatever' from='{jid_one}/{resource_one}' to='{irc_host_one}@{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[2]")),
- ]),
- Scenario("list_muc_user_adhoc",
- [
- handshake_sequence(),
- partial(send_stanza, "<iq type='get' id='idwhatever' from='{jid_admin}/{resource_one}' to='#foo%{irc_server_one}/{nick_one}'><query xmlns='http://jabber.org/protocol/disco#items' node='http://jabber.org/protocol/commands' /></iq>"),
- partial(expect_stanza, "/iq[@type='error']/error[@type='cancel']/stanza:feature-not-implemented"),
- ]
- ),
- Scenario("execute_hello_adhoc_command",
- [
- handshake_sequence(),
- partial(send_stanza, "<iq type='set' id='hello-command1' from='{jid_one}/{resource_one}' to='{biboumi_host}'><command xmlns='http://jabber.org/protocol/commands' node='hello' action='execute' /></iq>"),
- partial(expect_stanza, ("/iq[@type='result']/commands:command[@node='hello'][@sessionid][@status='executing']",
- "/iq/commands:command/dataform:x[@type='form']/dataform:title[text()='Configure your name.']",
- "/iq/commands:command/dataform:x[@type='form']/dataform:instructions[text()='Please provide your name.']",
- "/iq/commands:command/dataform:x[@type='form']/dataform:field[@type='text-single']/dataform:required",
- "/iq/commands:command/commands:actions/commands:next",
- ),
- after = partial(save_value, "sessionid", partial(extract_attribute, "/iq[@type='result']/commands:command[@node='hello']", "sessionid"))
- ),
- partial(send_stanza, "<iq type='set' id='hello-command2' from='{jid_one}/{resource_one}' to='{biboumi_host}'><command xmlns='http://jabber.org/protocol/commands' node='hello' sessionid='{sessionid}' action='next'><x xmlns='jabber:x:data' type='submit'><field var='name'><value>COUCOU</value></field></x></command></iq>"),
- partial(expect_stanza, "/iq[@type='result']/commands:command[@node='hello'][@status='completed']/commands:note[@type='info'][text()='Hello COUCOU!']")
- ]),
- Scenario("execute_incomplete_hello_adhoc_command",
- [
- handshake_sequence(),
- partial(send_stanza, "<iq type='set' id='hello-command1' from='{jid_one}/{resource_one}' to='{biboumi_host}'><command xmlns='http://jabber.org/protocol/commands' node='hello' action='execute' /></iq>"),
- partial(expect_stanza, ("/iq[@type='result']/commands:command[@node='hello'][@sessionid][@status='executing']",
- "/iq/commands:command/commands:actions/commands:next",
- ),
- after = partial(save_value, "sessionid", partial(extract_attribute, "/iq[@type='result']/commands:command[@node='hello']", "sessionid"))
- ),
- partial(send_stanza, "<iq type='set' id='hello-command2' from='{jid_one}/{resource_one}' to='{biboumi_host}'><command xmlns='http://jabber.org/protocol/commands' node='hello' sessionid='{sessionid}' action='next'><x xmlns='jabber:x:data' type='submit'></x></command></iq>"),
- partial(expect_stanza, "/iq[@type='error']")
- ]),
- Scenario("execute_ping_adhoc_command",
- [
- handshake_sequence(),
- partial(send_stanza, "<iq type='set' id='ping-command1' from='{jid_one}/{resource_one}' to='{biboumi_host}'><command xmlns='http://jabber.org/protocol/commands' node='ping' action='execute' /></iq>"),
- partial(expect_stanza, "/iq[@type='result']/commands:command[@node='ping'][@status='completed']/commands:note[@type='info'][text()='Pong']")
- ]),
- Scenario("execute_reload_adhoc_command",
- [
- handshake_sequence(),
- partial(send_stanza, "<iq type='set' id='ping-command1' from='{jid_admin}/{resource_one}' to='{biboumi_host}'><command xmlns='http://jabber.org/protocol/commands' node='reload' action='execute' /></iq>"),
- partial(expect_stanza, "/iq[@type='result']/commands:command[@node='reload'][@status='completed']/commands:note[@type='info'][text()='Configuration reloaded.']")
- ]),
- Scenario("execute_forbidden_adhoc_command",
- [
- handshake_sequence(),
- partial(send_stanza, "<iq type='set' id='command1' from='{jid_one}/{resource_one}' to='{biboumi_host}'><command xmlns='http://jabber.org/protocol/commands' node='disconnect-user' action='execute' /></iq>"),
- partial(expect_stanza, ("/iq[@type='error'][@id='command1']/commands:command[@node='disconnect-user']",
- "/iq/commands:command/commands:error[@type='cancel']/stanza:forbidden")),
- ]),
- Scenario("execute_disconnect_user_adhoc_command",
- [
- handshake_sequence(),
-
- partial(send_stanza, "<presence from='{jid_admin}/{resource_one}' to='#foo%{irc_server_one}/{nick_one}' />"),
- connection_sequence("irc.localhost", '{jid_admin}/{resource_one}'),
- partial(expect_stanza, "/message/body[text()='Mode #foo [+nt] by {irc_host_one}']"),
- partial(expect_stanza, "/presence"),
- partial(expect_stanza, "/message"),
-
- partial(send_stanza, "<iq type='set' id='command1' from='{jid_admin}/{resource_one}' to='{biboumi_host}'><command xmlns='http://jabber.org/protocol/commands' node='disconnect-user' action='execute' /></iq>"),
- partial(expect_stanza, ("/iq[@type='result']/commands:command[@node='disconnect-user'][@sessionid][@status='executing']",
- "/iq/commands:command/commands:actions/commands:next",
- ),
- after = partial(save_value, "sessionid", partial(extract_attribute, "/iq/commands:command[@node='disconnect-user']", "sessionid"))
- ),
- partial(send_stanza, "<iq type='set' id='command2' from='{jid_admin}/{resource_one}' to='{biboumi_host}'><command xmlns='http://jabber.org/protocol/commands' node='disconnect-user' sessionid='{sessionid}' action='next'><x xmlns='jabber:x:data' type='submit'><field var='jids'><value>{jid_admin}</value></field><field var='quit-message'><value>Disconnected by e2e</value></field></x></command></iq>"),
- partial(expect_stanza, "/iq[@type='result']/commands:command[@node='disconnect-user'][@status='completed']/commands:note[@type='info'][text()='1 user has been disconnected.']"),
- # Note, charybdis ignores our QUIT message, so we can't test it
- partial(expect_stanza, "/presence[@type='unavailable'][@to='{jid_admin}/{resource_one}'][@from='#foo%{irc_server_one}/{nick_one}']"),
- ]),
- Scenario("execute_admin_disconnect_from_server_adhoc_command",
- [
- handshake_sequence(),
-
- # Admin connects to first server
- partial(send_stanza, "<presence from='{jid_admin}/{resource_one}' to='#bar%{irc_server_one}/{nick_one}' />"),
- connection_sequence("irc.localhost", '{jid_admin}/{resource_one}'),
- partial(expect_stanza, "/message/body[text()='Mode #bar [+nt] by {irc_host_one}']"),
- partial(expect_stanza, "/presence"),
- partial(expect_stanza, "/message"),
-
- # Non-Admin connects to first server
- partial(send_stanza, "<presence from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}/{nick_two}' />"),
- 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"),
-
- # Non-admin connects to second server
- partial(send_stanza, "<presence from='{jid_one}/{resource_one}' to='#bon%{irc_server_two}/{nick_three}' />"),
- connection_sequence("localhost", '{jid_one}/{resource_one}'),
- partial(expect_stanza, "/message/body[text()='Mode #bon [+nt] by {irc_host_one}']"),
- partial(expect_stanza, "/presence"),
- partial(expect_stanza, "/message"),
-
- # Execute as admin
- partial(send_stanza, "<iq type='set' id='command1' from='{jid_admin}/{resource_one}' to='{biboumi_host}'><command xmlns='http://jabber.org/protocol/commands' node='disconnect-from-irc-server' action='execute' /></iq>"),
- partial(expect_stanza, ("/iq[@type='result']/commands:command[@node='disconnect-from-irc-server'][@sessionid][@status='executing']",
- "/iq/commands:command/dataform:x[@type='form']/dataform:field[@var='jid'][@type='list-single']/dataform:option[@label='{jid_one}']/dataform:value[text()='{jid_one}']",
- "/iq/commands:command/dataform:x[@type='form']/dataform:field[@var='jid'][@type='list-single']/dataform:option[@label='{jid_admin}']/dataform:value[text()='{jid_admin}']",
- "/iq/commands:command/commands:actions/commands:next",
- ),
- after = partial(save_value, "sessionid", partial(extract_attribute, "/iq/commands:command[@node='disconnect-from-irc-server']", "sessionid"))
- ),
- partial(send_stanza, "<iq type='set' id='command2' from='{jid_admin}/{resource_one}' to='{biboumi_host}'><command xmlns='http://jabber.org/protocol/commands' node='disconnect-from-irc-server' sessionid='{sessionid}' action='next'><x xmlns='jabber:x:data' type='submit'><field var='jid'><value>{jid_one}</value></field><field var='quit-message'><value>e2e test one</value></field></x></command></iq>"),
-
- partial(expect_stanza, ("/iq[@type='result']/commands:command[@node='disconnect-from-irc-server'][@sessionid][@status='executing']",
- "/iq/commands:command/dataform:x[@type='form']/dataform:field[@var='quit-message'][@type='text-single']",
- "/iq/commands:command/dataform:x[@type='form']/dataform:field[@var='irc-servers'][@type='list-multi']",
- "/iq/commands:command/dataform:x[@type='form']/dataform:field[@var='irc-servers']/dataform:option[@label='localhost']/dataform:value[text()='localhost']",
- "/iq/commands:command/dataform:x[@type='form']/dataform:field[@var='irc-servers']/dataform:option[@label='irc.localhost']/dataform:value[text()='irc.localhost']",
- "/iq/commands:command/commands:actions/commands:next",
- ),
- after = partial(save_value, "sessionid", partial(extract_attribute, "/iq/commands:command[@node='disconnect-from-irc-server']", "sessionid"))
- ),
- partial(send_stanza, "<iq type='set' id='command2' from='{jid_admin}/{resource_one}' to='{biboumi_host}'><command xmlns='http://jabber.org/protocol/commands' node='disconnect-from-irc-server' sessionid='{sessionid}' action='next'><x xmlns='jabber:x:data' type='submit'><field var='irc-servers'><value>localhost</value></field><field var='quit-message'><value>Disconnected by e2e</value></field></x></command></iq>"),
- partial(expect_unordered, [("/presence[@type='unavailable'][@to='{jid_one}/{resource_one}'][@from='#bon%{irc_server_two}/{nick_three}']",),
- ("/iq[@type='result']/commands:command[@node='disconnect-from-irc-server'][@status='completed']/commands:note[@type='info'][text()='{jid_one} was disconnected from 1 IRC server.']",),
- ]),
-
-
- # Execute as non-admin (this skips the first step)
- partial(send_stanza, "<iq type='set' id='command1' from='{jid_one}/{resource_one}' to='{biboumi_host}'><command xmlns='http://jabber.org/protocol/commands' node='disconnect-from-irc-server' action='execute' /></iq>"),
-
- partial(expect_stanza, ("/iq[@type='result']/commands:command[@node='disconnect-from-irc-server'][@sessionid][@status='executing']",
- "/iq/commands:command/dataform:x[@type='form']/dataform:field[@var='quit-message'][@type='text-single']",
- "/iq/commands:command/dataform:x[@type='form']/dataform:field[@var='irc-servers'][@type='list-multi']",
- "/iq/commands:command/dataform:x[@type='form']/dataform:field[@var='irc-servers']/dataform:option[@label='irc.localhost']/dataform:value[text()='irc.localhost']",
- "/iq/commands:command/commands:actions/commands:next",
- ),
- after = partial(save_value, "sessionid", partial(extract_attribute, "/iq/commands:command[@node='disconnect-from-irc-server']", "sessionid"))
- ),
- partial(send_stanza, "<iq type='set' id='command2' from='{jid_one}/{resource_one}' to='{biboumi_host}'><command xmlns='http://jabber.org/protocol/commands' node='disconnect-from-irc-server' sessionid='{sessionid}' action='next'><x xmlns='jabber:x:data' type='submit'><field var='irc-servers'><value>irc.localhost</value></field><field var='quit-message'><value>Disconnected by e2e</value></field></x></command></iq>"),
- partial(expect_unordered, [("/presence[@type='unavailable'][@to='{jid_one}/{resource_one}'][@from='#foo%{irc_server_one}/{nick_two}']",),
- ("/iq[@type='result']/commands:command[@node='disconnect-from-irc-server'][@status='completed']/commands:note[@type='info'][text()='{jid_one}/{resource_one} was disconnected from 1 IRC server.']",),
- ]),
- ]),
- Scenario("multisessionnick",
- [
- 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[@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'][@to='{jid_one}/{resource_one}']/subject[not(text())]"),
-
- # The other resources joins the same room, with the same nick
- partial(send_stanza,
- "<presence from='{jid_one}/{resource_two}' to='#foo%{irc_server_one}/{nick_one}' />"),
- # We receive our own join
- partial(expect_unordered,
- [("/presence[@to='{jid_one}/{resource_two}'][@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'][@to='{jid_one}/{resource_two}']/subject[not(text())]",)]
- ),
-
- # A different user joins the same room
- 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[@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[@to='{jid_two}/{resource_one}'][@from='#foo%{irc_server_one}/{nick_one}']",),
- ]
- ),
-
- 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
- partial(expect_stanza, ("/message[@from='{lower_nick_two}%{irc_server_one}'][@to='{jid_one}/{resource_one}'][@type='chat']/body[text()='RELLO']",
- "/message/hints:no-copy",
- "/message/carbon:private",
- "!/message/muc_user:x")),
- partial(expect_stanza, "/message[@from='{lower_nick_two}%{irc_server_one}'][@to='{jid_one}/{resource_two}'][@type='chat']/body[text()='RELLO']"),
-
-
- # First occupant (with the two resources) changes her/his nick
- partial(send_stanza, "<presence from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}/{nick_two}' />"),
- partial(expect_unordered, [
- ("/message[@to='{jid_one}/{resource_one}'][@type='chat']/body[text()='irc.localhost: Bobby: Nickname is already in use.']",),
- ("/message[@to='{jid_one}/{resource_two}'][@type='chat']/body[text()='irc.localhost: Bobby: Nickname is already in use.']",),
- ("/presence[@to='{jid_one}/{resource_one}'][@from='#foo%{irc_server_one}/{nick_two}'][@type='error']",),
- ("/presence[@to='{jid_one}/{resource_two}'][@from='#foo%{irc_server_one}/{nick_two}'][@type='error']",),
- ]),
-
- # First occupant (with the two resources) changes her/his nick
- partial(send_stanza, "<presence from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}/{nick_three}' />"),
- 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']"),
-
- ("/presence[@from='#foo%{irc_server_one}/{nick_one}'][@to='{jid_one}/{resource_two}'][@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_two}']",
- "/presence/muc_user:x/muc_user:status[@code='110']"),
- ]),
-
- # One resource leaves the server entirely.
- partial(send_stanza, "<presence type='unavailable' from='{jid_one}/{resource_two}' to='#foo%{irc_server_one}/{nick_one}' />"),
- # The leave is forwarded only to us
- partial(expect_stanza,
- ("/presence[@type='unavailable']/muc_user:x/muc_user:status[@code='110']",
- "/presence/status[text()='Biboumi note: 1 resources are still in this channel.']",
- )
- ),
-
- # The second user sends two new private messages to the first user
- partial(send_stanza, "<message from='{jid_two}/{resource_one}' to='#foo%{irc_server_one}/{nick_three}' type='chat'><body>first</body></message>"),
- partial(send_stanza, "<message from='{jid_two}/{resource_one}' to='#foo%{irc_server_one}/{nick_three}' type='chat'><body>second</body></message>"),
- # The first user receives the two messages, on the connected resource, once each
-
- partial(expect_unordered, [
- ("/message[@from='{lower_nick_two}%{irc_server_one}'][@to='{jid_one}/{resource_one}'][@type='chat']/body[text()='first']",),
- ("/message[@from='{lower_nick_two}%{irc_server_one}'][@to='{jid_one}/{resource_one}'][@type='chat']/body[text()='second']",),
- ]),
- ]),
- Scenario("persistent_channel",
- [
- # Join the channel with user 1
- 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[@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'][@to='{jid_one}/{resource_one}']/subject[not(text())]"),
-
- # Make it persistent for user 1
- partial(send_stanza, "<iq from='{jid_one}/{resource_one}' id='conf1' to='#foo%{irc_server_one}' type='get'><query xmlns='http://jabber.org/protocol/muc#owner'/></iq>"),
- partial(expect_stanza, "/iq[@type='result']/muc_owner:query/dataform:x/dataform:field[@var='persistent'][@type='boolean']/dataform:value[text()='false']"),
- partial(send_stanza, "<iq from='{jid_one}/{resource_one}' id='conf2' to='#foo%{irc_server_one}' type='set'><query xmlns='http://jabber.org/protocol/muc#owner'><x type='submit' xmlns='jabber:x:data'><field var='persistent' xmlns='jabber:x:data'><value>true</value></field></x></query></iq>"),
- partial(expect_stanza, "/iq[@type='result']"),
-
- # Check that the value is now effectively true
- partial(send_stanza, "<iq from='{jid_one}/{resource_one}' id='conf1' to='#foo%{irc_server_one}' type='get'><query xmlns='http://jabber.org/protocol/muc#owner'/></iq>"),
- partial(expect_stanza, "/iq[@type='result']/muc_owner:query/dataform:x/dataform:field[@var='persistent'][@type='boolean']/dataform:value[text()='true']"),
-
- # A second user joins the same channel
- 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[@to='{jid_one}/{resource_one}'][@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[@to='{jid_two}/{resource_one}'][@from='#foo%{irc_server_one}/{nick_one}']",),
- ]
- ),
-
- partial(expect_stanza, "/message[@from='#foo%{irc_server_one}'][@type='groupchat']/subject[not(text())]"),
-
- # First user leaves the room (but biboumi will stay in the channel)
- partial(send_stanza,
- "<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/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>"),
-
- # Message should only be received by user 2, since user 1 has no resource in the room
- partial(expect_stanza, "/message[@type='groupchat'][@to='{jid_two}/{resource_one}'][@from='#foo%{irc_server_one}/{nick_two}']"),
-
- # Second user leaves the channel
- partial(send_stanza, "<presence type='unavailable' from='{jid_two}/{resource_one}' to='#foo%{irc_server_one}/{nick_two}' />"),
- partial(expect_stanza, "/presence[@type='unavailable'][@from='#foo%{irc_server_one}/{nick_two}']"),
- ]),
- Scenario("channel_join_with_different_nick",
- [
- 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[@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'][@to='{jid_one}/{resource_one}']/subject[not(text())]"),
-
- # The same resource joins a different channel with a different nick
- partial(send_stanza,
- "<presence from='{jid_one}/{resource_one}' to='#bar%{irc_server_one}/{nick_two}' />"),
-
- # We must receive a join presence in response, without any nick change (nick_two) must be ignored
- partial(expect_stanza,
- "/message/body[text()='Mode #bar [+nt] by {irc_host_one}']"),
- partial(expect_stanza,
- ("/presence[@to='{jid_one}/{resource_one}'][@from='#bar%{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='#bar%{irc_server_one}'][@type='groupchat'][@to='{jid_one}/{resource_one}']/subject[not(text())]"),
- ]),
- Scenario("notices",
- [
- 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"),
- partial(expect_stanza, "/presence"),
- partial(expect_stanza, "/message"),
-
- 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']"),
-
- # 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>un\ndeux\ntrois</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()='un']",),
- ("/message[@from='#foo%{irc_server_one}/{nick_one}'][@id][@to='{jid_one}/{resource_one}'][@type='groupchat']/body[text()='deux']",),
- ("/message[@from='#foo%{irc_server_one}/{nick_one}'][@id][@to='{jid_one}/{resource_one}'][@type='groupchat']/body[text()='trois']",),
-
- ("/message[@from='#foo%{irc_server_one}/{nick_one}'][@id][@to='{jid_two}/{resource_one}'][@type='groupchat']/body[text()='un']",),
- ("/message[@from='#foo%{irc_server_one}/{nick_one}'][@id][@to='{jid_two}/{resource_one}'][@type='groupchat']/body[text()='deux']",),
- ("/message[@from='#foo%{irc_server_one}/{nick_one}'][@id][@to='{jid_two}/{resource_one}'][@type='groupchat']/body[text()='trois']",),
- ])
- ]),
- Scenario("channel_messages",
- [
- 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())]"),
-
- # 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 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, 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/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
- partial(send_stanza, "<message from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}/{nick_two}' type='chat'><body>coucou 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()='coucou 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>yes</body></message>"),
- # The response is received from the in-room JID
- partial(expect_stanza, ("/message[@from='#foo%{irc_server_one}/{nick_two}'][@to='{jid_one}/{resource_one}'][@type='chat']/body[text()='yes']",
- "/message/muc_user:x")),
-
- ## Do the exact same thing, from a different chan,
- # to check if the response comes from the right JID
-
- partial(send_stanza,
- "<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='#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='#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='#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='#dummy%{irc_server_one}/{nick_one}' type='unavailable' />"),
- partial(expect_stanza,
- "/presence[@type='unavailable']/muc_user:x/muc_user:status[@code='110']"),
-
- # The private messages from this nick should now come (again) from the server-wide JID
- partial(send_stanza, "<message from='{jid_two}/{resource_one}' to='{lower_nick_one}%{irc_server_one}' type='chat'><body>hihihoho</body></message>"),
- partial(expect_stanza,
- "/message[@from='{lower_nick_two}%{irc_server_one}'][@to='{jid_one}/{resource_one}']"),
- ]
- ),
- Scenario("encoded_channel_join",
- [
- handshake_sequence(),
- partial(send_stanza,
- "<presence from='{jid_one}/{resource_one}' to='#biboumi\\40louiz.org\\3a80%{irc_server_one}/{nick_one}' />"),
- connection_sequence("irc.localhost", '{jid_one}/{resource_one}'),
- partial(expect_stanza,
- "/message/body[text()='Mode #biboumi@louiz.org:80 [+nt] by {irc_host_one}']"),
- partial(expect_stanza,
- ("/presence[@to='{jid_one}/{resource_one}'][@from='#biboumi\\40louiz.org\\3a80%{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='#biboumi\\40louiz.org\\3a80%{irc_server_one}'][@type='groupchat']/subject[not(text())]"),
- ]),
- Scenario("self_ping_with_error",
- [
- 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[@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 ping to ourself
- partial(send_stanza,
- "<iq type='get' from='{jid_one}/{resource_one}' id='first_ping' to='#foo%{irc_server_one}/{nick_one}'><ping xmlns='urn:xmpp:ping' /></iq>"),
- # We receive our own ping request,
- partial(expect_stanza,
- "/iq[@from='{lower_nick_one}%{irc_server_one}'][@type='get'][@to='{jid_one}/{resource_one}'][@id='gnip_tsrif']"),
- # Respond to the request with an error
- partial(send_stanza,
- "<iq from='{jid_one}/{resource_one}' id='gnip_tsrif' to='{lower_nick_one}%{irc_server_one}' type='error'><error type='cancel'><feature-not-implemented xmlns='urn:ietf:params:xml:ns:xmpp-stanzas'/></error></iq>"),
- partial(expect_stanza,
- "/iq[@from='#foo%{irc_server_one}/{nick_one}'][@type='result'][@to='{jid_one}/{resource_one}'][@id='first_ping']"),
-
- # Send a ping to ourself
- partial(send_stanza,
- "<iq type='get' from='{jid_one}/{resource_one}' id='first_ping' to='#foo%{irc_server_one}/{nick_one}'><ping xmlns='urn:xmpp:ping' /></iq>"),
- # We receive our own ping request,
- partial(expect_stanza,
- "/iq[@from='{lower_nick_one}%{irc_server_one}'][@type='get'][@to='{jid_one}/{resource_one}'][@id='gnip_tsrif']"),
- # Respond to the request with an error
- partial(send_stanza,
- "<iq from='{jid_one}/{resource_one}' id='gnip_tsrif' to='{lower_nick_one}%{irc_server_one}' type='error'><error type='cancel'><service-unavailable xmlns='urn:ietf:params:xml:ns:xmpp-stanzas'/></error></iq>"),
- partial(expect_stanza,
- "/iq[@from='#foo%{irc_server_one}/{nick_one}'][@type='result'][@to='{jid_one}/{resource_one}'][@id='first_ping']"),
- ]),
- Scenario("self_ping_not_in_muc",
- [
- 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[@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())]"),
+ provided_scenar_names = sys.argv[1:]
+ scenarios = get_scenarios(os.path.abspath(os.path.dirname(__file__)), provided_scenar_names)
- # Send a ping to ourself, in a muc where we’re not
- partial(send_stanza,
- "<iq type='get' from='{jid_one}/{resource_one}' id='first_ping' to='#nil%{irc_server_one}/{nick_one}'><ping xmlns='urn:xmpp:ping' /></iq>"),
- # Immediately receive an error
- partial(expect_stanza,
- "/iq[@from='#nil%{irc_server_one}/{nick_one}'][@type='error'][@to='{jid_one}/{resource_one}'][@id='first_ping']/error/stanza:not-allowed"),
-
- # Send a ping to ourself, in a muc where we are, but not this resource
- partial(send_stanza,
- "<iq type='get' from='{jid_one}/{resource_two}' id='first_ping' to='#foo%{irc_server_one}/{nick_one}'><ping xmlns='urn:xmpp:ping' /></iq>"),
- # Immediately receive an error
- partial(expect_stanza,
- "/iq[@from='#foo%{irc_server_one}/{nick_one}'][@type='error'][@to='{jid_one}/{resource_two}'][@id='first_ping']/error/stanza:not-allowed"),
- ]),
- Scenario("self_ping_on_real_channel",
- [
- 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[@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 ping to ourself
- partial(send_stanza,
- "<iq type='get' from='{jid_one}/{resource_one}' id='first_ping' to='#foo%{irc_server_one}/{nick_one}'><ping xmlns='urn:xmpp:ping' /></iq>"),
- # We receive our own ping request,
- partial(expect_stanza,
- "/iq[@from='{lower_nick_one}%{irc_server_one}'][@type='get'][@to='{jid_one}/{resource_one}'][@id='gnip_tsrif']"),
- # Respond to the request
- partial(send_stanza,
- "<iq type='result' to='{lower_nick_one}%{irc_server_one}' id='gnip_tsrif' from='{jid_one}/{resource_one}'/>"),
- partial(expect_stanza,
- "/iq[@from='#foo%{irc_server_one}/{nick_one}'][@type='result'][@to='{jid_one}/{resource_one}'][@id='first_ping']"),
-
- # Now join the same room, from the same bare JID, behind the same nick
- partial(send_stanza,
- "<presence from='{jid_one}/{resource_two}' to='#foo%{irc_server_one}/{nick_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'][@role='moderator']",
- "/presence/muc_user:x/muc_user:status[@code='110']")
- ),
- partial(expect_stanza, "/message[@from='#foo%{irc_server_one}'][@type='groupchat'][@to='{jid_one}/{resource_two}']/subject[not(text())]"),
-
- # And re-send a self ping
- partial(send_stanza,
- "<iq type='get' from='{jid_one}/{resource_one}' id='second_ping' to='#foo%{irc_server_one}/{nick_one}'><ping xmlns='urn:xmpp:ping' /></iq>"),
- # We receive our own ping request. Note that we don't know the to value, it could be one of our two resources.
- partial(expect_stanza,
- "/iq[@from='{lower_nick_one}%{irc_server_one}'][@type='get'][@to][@id='gnip_dnoces']",
- after = partial(save_value, "to", partial(extract_attribute, "/iq", "to"))),
- # Respond to the request, using the extracted 'to' value as our 'from'
- partial(send_stanza,
- "<iq type='result' to='{lower_nick_one}%{irc_server_one}' id='gnip_dnoces' from='{to}'/>"),
- partial(expect_stanza,
- "/iq[@from='#foo%{irc_server_one}/{nick_one}'][@type='result'][@to='{jid_one}/{resource_one}'][@id='second_ping']"),
- ## And re-do exactly the same thing, just change the resource initiating the self ping
- partial(send_stanza,
- "<iq type='get' from='{jid_one}/{resource_two}' id='third_ping' to='#foo%{irc_server_one}/{nick_one}'><ping xmlns='urn:xmpp:ping' /></iq>"),
- partial(expect_stanza,
- "/iq[@from='{lower_nick_one}%{irc_server_one}'][@type='get'][@to][@id='gnip_driht']",
- after = partial(save_value, "to", partial(extract_attribute, "/iq", "to"))),
- partial(send_stanza,
- "<iq type='result' to='{lower_nick_one}%{irc_server_one}' id='gnip_driht' from='{to}'/>"),
- partial(expect_stanza,
- "/iq[@from='#foo%{irc_server_one}/{nick_one}'][@type='result'][@to='{jid_one}/{resource_two}'][@id='third_ping']"),
-
- ]),
- Scenario("self_ping_fixed_server", [
- handshake_sequence(),
- partial(send_stanza,
- "<presence from='{jid_one}/{resource_one}' to='#foo@{biboumi_host}/{nick_one}' />"),
- connection_sequence("irc.localhost", '{jid_one}/{resource_one}', fixed_irc_server=True),
- partial(expect_stanza,
- "/message/body[text()='Mode #foo [+nt] by {irc_host_one}']"),
- partial(expect_stanza,
- ("/presence[@to='{jid_one}/{resource_one}'][@from='#foo@{biboumi_host}/{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@{biboumi_host}'][@type='groupchat']/subject[not(text())]"),
-
- # Send a ping to ourself
- partial(send_stanza,
- "<iq type='get' from='{jid_one}/{resource_one}' id='first_ping' to='#foo@{biboumi_host}/{nick_one}'><ping xmlns='urn:xmpp:ping' /></iq>"),
- # We receive our own ping request,
- partial(expect_stanza,
- "/iq[@from='{lower_nick_one}@{biboumi_host}'][@type='get'][@to='{jid_one}/{resource_one}'][@id='gnip_tsrif']"),
- # Respond to the request
- partial(send_stanza,
- "<iq type='result' to='{lower_nick_one}@{biboumi_host}' id='gnip_tsrif' from='{jid_one}/{resource_one}'/>"),
- partial(expect_stanza,
- "/iq[@from='#foo@{biboumi_host}/{nick_one}'][@type='result'][@to='{jid_one}/{resource_one}'][@id='first_ping']"),
- ], conf="fixed_server"),
- Scenario("simple_kick",
- [
- 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"),
- partial(expect_stanza, "/presence/muc_user:x/muc_user:status[@code='110']"),
- partial(expect_stanza, "/message[@type='groupchat']/subject"),
-
- # 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/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']",),
- ("/message/subject",),
- ]),
-
- # demonstrate bug https://lab.louiz.org/louiz/biboumi/issues/3291
- # First user joins an other channel
- partial(send_stanza,
- "<presence from='{jid_one}/{resource_one}' to='#bar%{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[@type='groupchat']/subject"),
-
- # Second user joins
- 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']",),
- ("/message/subject",),
- ]),
-
- # Moderator kicks participant
- partial(send_stanza,
- "<iq id='kick1' to='#foo%{irc_server_one}' from='{jid_one}/{resource_one}' type='set'><query xmlns='http://jabber.org/protocol/muc#admin'><item nick='{nick_two}' role='none'><reason>reported</reason></item></query></iq>"),
- partial(expect_unordered, [
- ("/presence[@type='unavailable'][@to='{jid_two}/{resource_one}']/muc_user:x/muc_user:item[@role='none']/muc_user:actor[@nick='{nick_one}']",
- "/presence/muc_user:x/muc_user:item/muc_user:reason[text()='reported']",
- "/presence/muc_user:x/muc_user:status[@code='307']",
- "/presence/muc_user:x/muc_user:status[@code='110']"
- ),
- ("/presence[@type='unavailable'][@to='{jid_one}/{resource_one}']/muc_user:x/muc_user:item[@role='none']/muc_user:actor[@nick='{nick_one}']",
- "/presence/muc_user:x/muc_user:item/muc_user:reason[text()='reported']",
- "/presence/muc_user:x/muc_user:status[@code='307']",
- ),
- ("/iq[@id='kick1'][@type='result']",),
- ]),
-
- # Bug 3291, suite. We must not receive any presence from #foo, here
- partial(send_stanza, "<message from='{jid_two}/{resource_one}' to='{irc_server_one}' type='chat'><body>QUIT bye bye</body></message>"),
- partial(expect_unordered,
- [("/presence[@from='#bar%{irc_server_one}/{nick_two}'][@to='{jid_one}/{resource_one}']",),
- ("/presence[@from='#bar%{irc_server_one}/{nick_two}'][@to='{jid_two}/{resource_one}']",),
- ("/message",),
- ("/message",)])
- ]),
- Scenario("mode_change",
- [
- 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"),
- partial(expect_stanza, "/presence/muc_user:x/muc_user:status[@code='110']"),
- partial(expect_stanza, "/message[@type='groupchat']/subject"),
-
- # 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/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']",),
- ("/message/subject",),
- ]),
-
- # Change a user mode with a message starting with /mode
- partial(send_stanza,
- "<message from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}' type='groupchat'><body>/mode +v {nick_two}</body></message>"),
- partial(expect_unordered, [
- ("/message[@to='{jid_one}/{resource_one}']/body[text()='Mode #foo [+v {nick_two}] by {nick_one}']",),
- ("/message[@to='{jid_two}/{resource_one}']/body[text()='Mode #foo [+v {nick_two}] by {nick_one}']",),
- ("/presence[@to='{jid_two}/{resource_one}'][@from='#foo%{irc_server_one}/{nick_two}']/muc_user:x/muc_user:item[@affiliation='member'][@role='participant']",),
- ("/presence[@to='{jid_one}/{resource_one}'][@from='#foo%{irc_server_one}/{nick_two}']/muc_user:x/muc_user:item[@affiliation='member'][@role='participant']",)
- ]),
-
- # using an iq
- partial(send_stanza,
- "<iq from='{jid_one}/{resource_one}' id='id1' to='#foo%{irc_server_one}' type='set'><query xmlns='http://jabber.org/protocol/muc#admin'><item affiliation='admin' nick='{nick_two}'/></query></iq>"),
- partial(expect_unordered, [
- ("/message[@to='{jid_one}/{resource_one}']/body[text()='Mode #foo [+o {nick_two}] by {nick_one}']",),
- ("/message[@to='{jid_two}/{resource_one}']/body[text()='Mode #foo [+o {nick_two}] by {nick_one}']",),
- ("/presence[@to='{jid_two}/{resource_one}'][@from='#foo%{irc_server_one}/{nick_two}']/muc_user:x/muc_user:item[@affiliation='admin'][@role='moderator']",),
- ("/presence[@to='{jid_one}/{resource_one}'][@from='#foo%{irc_server_one}/{nick_two}']/muc_user:x/muc_user:item[@affiliation='admin'][@role='moderator']",),
- ("/iq[@id='id1'][@type='result'][@to='{jid_one}/{resource_one}'][@from='#foo%{irc_server_one}']",),
- ]),
-
- # remove the mode
- partial(send_stanza,
- "<iq from='{jid_one}/{resource_one}' id='id1' to='#foo%{irc_server_one}' type='set'><query xmlns='http://jabber.org/protocol/muc#admin'><item affiliation='member' nick='{nick_two}' role='participant'/></query></iq>"),
- partial(expect_unordered, [
- ("/message[@to='{jid_one}/{resource_one}']/body[text()='Mode #foo [+v-o {nick_two} {nick_two}] by {nick_one}']",),
- ("/message[@to='{jid_two}/{resource_one}']/body[text()='Mode #foo [+v-o {nick_two} {nick_two}] by {nick_one}']",),
- ("/presence[@to='{jid_two}/{resource_one}'][@from='#foo%{irc_server_one}/{nick_two}']/muc_user:x/muc_user:item[@affiliation='member'][@role='participant']",),
- ("/presence[@to='{jid_one}/{resource_one}'][@from='#foo%{irc_server_one}/{nick_two}']/muc_user:x/muc_user:item[@affiliation='member'][@role='participant']",),
- ("/iq[@id='id1'][@type='result'][@to='{jid_one}/{resource_one}'][@from='#foo%{irc_server_one}']",),
- ]),
-
- # using an iq, an a non-existant nick
- partial(send_stanza,
- "<iq from='{jid_one}/{resource_one}' id='id1' to='#foo%{irc_server_one}' type='set'><query xmlns='http://jabber.org/protocol/muc#admin'><item affiliation='admin' nick='blectre'/></query></iq>"),
- partial(expect_stanza, "/iq[@type='error']"),
-
- # using an iq, without the rights to do it
- partial(send_stanza,
- "<iq from='{jid_two}/{resource_one}' id='id1' to='#foo%{irc_server_one}' type='set'><query xmlns='http://jabber.org/protocol/muc#admin'><item affiliation='admin' nick='{nick_one}'/></query></iq>"),
- partial(expect_unordered, [
- ("/iq[@type='error']",),
- ("/message[@type='chat'][@to='{jid_two}/{resource_one}']",),
- ]),
-
- # using an iq, with an unknown mode
- partial(send_stanza,
- "<iq from='{jid_two}/{resource_one}' id='id1' to='#foo%{irc_server_one}' type='set'><query xmlns='http://jabber.org/protocol/muc#admin'><item affiliation='owner' nick='{nick_one}'/></query></iq>"),
- partial(expect_unordered, [
- ("/iq[@type='error']",),
- ("/message[@type='chat'][@to='{jid_two}/{resource_one}']",),
- ]),
-
- ]),
- Scenario("multisession_kick",
- [
- 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"),
- 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
- 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/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']",),
- ("/message/subject",),
- ]),
-
- partial(send_stanza,
- "<presence from='{jid_two}/{resource_two}' to='#foo%{irc_server_one}/{nick_two}' />"),
- partial(expect_stanza,
- "/presence[@to='{jid_two}/{resource_two}'][@from='#foo%{irc_server_one}/{nick_one}']"),
- partial(expect_stanza,
- ("/presence[@to='{jid_two}/{resource_two}'][@from='#foo%{irc_server_one}/{nick_two}']",
- "/presence/muc_user:x/muc_user:status[@code='110']")
- ),
- partial(expect_stanza, "/message[@from='#foo%{irc_server_one}'][@type='groupchat'][@to='{jid_two}/{resource_two}']/subject[not(text())]"),
-
- # Moderator kicks participant
- partial(send_stanza,
- "<iq id='kick1' to='#foo%{irc_server_one}' from='{jid_one}/{resource_one}' type='set'><query xmlns='http://jabber.org/protocol/muc#admin'><item nick='{nick_two}' role='none'><reason>reported</reason></item></query></iq>"),
- partial(expect_unordered, [
- ("/presence[@type='unavailable'][@to='{jid_two}/{resource_one}']/muc_user:x/muc_user:item[@role='none']/muc_user:actor[@nick='{nick_one}']",
- "/presence/muc_user:x/muc_user:item/muc_user:reason[text()='reported']",
- "/presence/muc_user:x/muc_user:status[@code='307']",
- "/presence/muc_user:x/muc_user:status[@code='110']"
- ),
- ("/presence[@type='unavailable'][@to='{jid_two}/{resource_two}']/muc_user:x/muc_user:item[@role='none']/muc_user:actor[@nick='{nick_one}']",
- "/presence/muc_user:x/muc_user:item/muc_user:reason[text()='reported']",
- "/presence/muc_user:x/muc_user:status[@code='307']",
- "/presence/muc_user:x/muc_user:status[@code='110']"
- ),
- ("/presence[@type='unavailable']/muc_user:x/muc_user:item[@role='none']/muc_user:actor[@nick='{nick_one}']",
- "/presence/muc_user:x/muc_user:item/muc_user:reason[text()='reported']",
- "/presence/muc_user:x/muc_user:status[@code='307']",
- ),
- ("/iq[@id='kick1'][@type='result']",),
- ]),
- ]),
- Scenario("self_version",
- [
- 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[@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 version request to ourself
- partial(send_stanza,
- "<iq type='get' from='{jid_one}/{resource_one}' id='first_version' to='#foo%{irc_server_one}/{nick_one}'><query xmlns='jabber:iq:version' /></iq>"),
- # We receive our own request,
- partial(expect_stanza,
- "/iq[@from='{lower_nick_one}%{irc_server_one}'][@type='get'][@to='{jid_one}/{resource_one}']",
- after = partial(save_value, "id", partial(extract_attribute, "/iq", 'id'))),
- # Respond to the request
- partial(send_stanza,
- "<iq type='result' to='{lower_nick_one}%{irc_server_one}' id='{id}' from='{jid_one}/{resource_one}'><query xmlns='jabber:iq:version'><name>e2e test</name><version>1.0</version><os>Fedora</os></query></iq>"),
- partial(expect_stanza,
- "/iq[@from='#foo%{irc_server_one}/{nick_one}'][@type='result'][@to='{jid_one}/{resource_one}'][@id='first_version']/version:query/version:name[text()='e2e test (through the biboumi gateway) 1.0 Fedora']"),
-
- # Now join the same room, from the same bare JID, behind the same nick
- partial(send_stanza,
- "<presence from='{jid_one}/{resource_two}' to='#foo%{irc_server_one}/{nick_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'][@role='moderator']",
- "/presence/muc_user:x/muc_user:status[@code='110']")
- ),
- partial(expect_stanza, "/message[@from='#foo%{irc_server_one}'][@type='groupchat'][@to='{jid_one}/{resource_two}']/subject[not(text())]"),
-
- # And re-send a self ping
- partial(send_stanza,
- "<iq type='get' from='{jid_one}/{resource_two}' id='second_version' to='#foo%{irc_server_one}/{nick_one}'><query xmlns='jabber:iq:version' /></iq>"),
- # We receive our own request. Note that we don't know the to value, it could be one of our two resources.
- partial(expect_stanza,
- "/iq[@from='{lower_nick_one}%{irc_server_one}'][@type='get'][@to]",
- after = (partial(save_value, "to", partial(extract_attribute, "/iq", "to")),
- partial(save_value, "id", partial(extract_attribute, "/iq", "id")))),
- # Respond to the request, using the extracted 'to' value as our 'from'
- partial(send_stanza,
- "<iq type='result' to='{lower_nick_one}%{irc_server_one}' id='{id}' from='{to}'><query xmlns='jabber:iq:version'><name>e2e test</name><version>1.0</version><os>Fedora</os></query></iq>"),
- partial(expect_stanza,
- "/iq[@from='#foo%{irc_server_one}/{nick_one}'][@type='result'][@to='{jid_one}/{resource_two}'][@id='second_version']"),
-
- # And do exactly the same thing, but initiated by the other resource
- partial(send_stanza,
- "<iq type='get' from='{jid_one}/{resource_one}' id='second_version' to='#foo%{irc_server_one}/{nick_one}'><query xmlns='jabber:iq:version' /></iq>"),
- # We receive our own request. Note that we don't know the to value, it could be one of our two resources.
- partial(expect_stanza,
- "/iq[@from='{lower_nick_one}%{irc_server_one}'][@type='get'][@to]",
- after = (partial(save_value, "to", partial(extract_attribute, "/iq", "to")),
- partial(save_value, "id", partial(extract_attribute, "/iq", "id")))),
- # Respond to the request, using the extracted 'to' value as our 'from'
- partial(send_stanza,
- "<iq type='result' to='{lower_nick_one}%{irc_server_one}' id='{id}' from='{to}'><query xmlns='jabber:iq:version'><name>e2e test</name><version>1.0</version><os>Fedora</os></query></iq>"),
- partial(expect_stanza,
- "/iq[@from='#foo%{irc_server_one}/{nick_one}'][@type='result'][@to='{jid_one}/{resource_one}'][@id='second_version']"),
- ]),
- Scenario("version_on_global_nick",
- [
- 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[@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 type='get' from='{jid_one}/{resource_one}' id='first_version' to='{lower_nick_one}%{irc_server_one}'><query xmlns='jabber:iq:version' /></iq>"),
-
- partial(expect_stanza,
- "/iq[@from='{lower_nick_one}%{irc_server_one}'][@type='get'][@to='{jid_one}/{resource_one}']",
- after = partial(save_value, "id", partial(extract_attribute, "/iq", 'id'))),
- partial(send_stanza,
- "<iq type='result' to='{lower_nick_one}%{irc_server_one}' id='{id}' from='{jid_one}/{resource_one}'><query xmlns='jabber:iq:version'><name>e2e test</name><version>1.0</version><os>Fedora</os></query></iq>"),
- partial(expect_stanza,
- "/iq[@from='{lower_nick_one}%{irc_server_one}'][@type='result'][@to='{jid_one}/{resource_one}'][@id='first_version']/version:query/version:name[text()='e2e test (through the biboumi gateway) 1.0 Fedora']"),
-
- ]),
- Scenario("self_invite",
- [
- 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[@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,
- "<message from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}'><x xmlns='http://jabber.org/protocol/muc#user'><invite to='{nick_one}'/></x></message>"),
- partial(expect_stanza,
- "/message/body[text()='{nick_one} is already on channel #foo']")
- ]),
- Scenario("client_error",
- [
- handshake_sequence(),
- # First resource
- 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())]"),
-
- # Second resource, same channel
- partial(send_stanza,
- "<presence from='{jid_one}/{resource_two}' to='#foo%{irc_server_one}/{nick_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'][@role='moderator']",
- "/presence/muc_user:x/muc_user:status[@code='110']")
- ),
- partial(expect_stanza, "/message[@from='#foo%{irc_server_one}'][@type='groupchat'][@to='{jid_one}/{resource_two}']/subject[not(text())]"),
-
- # Now the first resource has an error
- partial(send_stanza,
- "<message from='{jid_one}/{resource_one}' to='#foo%%{irc_server_one}/{nick_one}' type='error'><error type='cancel'><recipient-unavailable xmlns='urn:ietf:params:xml:ns:xmpp-stanzas'/></error></message>"),
- # Receive a leave only to the leaving resource
- partial(expect_stanza,
- ("/presence[@type='unavailable'][@from='#foo%{irc_server_one}/{nick_one}'][@to='{jid_one}/{resource_one}']/muc_user:x/muc_user:status[@code='110']",
- "/presence/status[text()='Biboumi note: 1 resources are still in this channel.']")
- ),
- ]),
- Scenario("simple_mam",
- [
- handshake_sequence(),
- 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 two channel messages
- partial(send_stanza, "<message from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}' type='groupchat'><body>coucou</body></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]",)
- ),
-
- partial(send_stanza, "<message from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}' type='groupchat'><body>coucou 2</body></message>"),
- partial(expect_stanza, "/message[@from='#foo%{irc_server_one}/{nick_one}'][@to='{jid_one}/{resource_one}'][@type='groupchat']/body[text()='coucou 2']"),
-
- # Retrieve the complete archive
- partial(send_stanza, "<iq to='#foo%{irc_server_one}' from='{jid_one}/{resource_one}' type='set' id='id1'><query xmlns='urn:xmpp:mam:2' queryid='qid1' /></iq>"),
-
- partial(expect_stanza,
- ("/message/mam:result[@queryid='qid1']/forward:forwarded/delay:delay",
- "/message/mam:result[@queryid='qid1']/forward:forwarded/client:message[@from='#foo%{irc_server_one}/{nick_one}'][@type='groupchat']/client:body[text()='coucou']")
- ),
- partial(expect_stanza,
- ("/message/mam:result[@queryid='qid1']/forward:forwarded/delay:delay",
- "/message/mam:result[@queryid='qid1']/forward:forwarded/client:message[@from='#foo%{irc_server_one}/{nick_one}'][@type='groupchat']/client:body[text()='coucou 2']")
- ),
-
- partial(expect_stanza,
- ("/iq[@type='result'][@id='id1'][@from='#foo%{irc_server_one}'][@to='{jid_one}/{resource_one}']",
- "/iq/mam:fin/rms:set/rsm:last",
- "/iq/mam:fin/rsm:set/rsm:first",
- "/iq/mam:fin[@complete='true']")),
-
- # Retrieve an empty archive by specifying an early “end” date
- 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'>
- <x xmlns='jabber:x:data' type='submit'>
- <field var='FORM_TYPE' type='hidden'> <value>urn:xmpp:mam:2</value></field>
- <field var='end'><value>2000-06-07T00:00:00Z</value></field>
- </x>
- </query></iq>"""),
-
- partial(expect_stanza,
- ("/iq[@type='result'][@id='id2'][@from='#foo%{irc_server_one}'][@to='{jid_one}/{resource_one}']",
- "/iq/mam:fin[@complete='true']/rsm:set",)),
-
- # Retrieve an empty archive by specifying a late “start” date
- # (note that this test will break in ~1000 years)
- 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'>
- <x xmlns='jabber:x:data' type='submit'>
- <field var='FORM_TYPE' type='hidden'> <value>urn:xmpp:mam:2</value></field>
- <field var='start'><value>3016-06-07T00:00:00Z</value></field>
- </x>
- </query></iq>"""),
-
- partial(expect_stanza,
- ("/iq[@type='result'][@id='id3'][@from='#foo%{irc_server_one}'][@to='{jid_one}/{resource_one}']",
- "/iq/mam:fin[@complete='true']/rsm:set")),
-
- # 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']")
- ),
-
- 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")),
-
- ]),
- Scenario("mam_with_timestamps",
- [
- 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[@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 two channel messages
- partial(send_stanza, "<message from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}' type='groupchat'><body>coucou</body></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]",)
- ),
-
- partial(send_stanza, "<message from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}' type='groupchat'><body>coucou 2</body></message>"),
- # Record the current time
- partial(expect_stanza, "/message[@from='#foo%{irc_server_one}/{nick_one}'][@to='{jid_one}/{resource_one}'][@type='groupchat']/body[text()='coucou 2']",
- after = partial(save_current_timestamp_plus_delta, "first_timestamp", datetime.timedelta(seconds=1))),
-
- # Wait two seconds before sending two new messages
- partial(sleep_for, 2),
- partial(send_stanza, "<message from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}' type='groupchat'><body>coucou 3</body></message>"),
- partial(send_stanza, "<message from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}' type='groupchat'><body>coucou 4</body></message>"),
- partial(expect_stanza, "/message[@type='groupchat']/body[text()='coucou 3']"),
- partial(expect_stanza, "/message[@type='groupchat']/body[text()='coucou 4']",
- after = partial(save_current_timestamp_plus_delta, "second_timestamp", datetime.timedelta(seconds=1))),
-
- # Retrieve the archive, after our saved datetime
- partial(send_stanza, """<iq to='#foo%{irc_server_one}' from='{jid_one}/{resource_one}' type='set' id='id8'>
- <query xmlns='urn:xmpp:mam:2' queryid='qid16'>
- <x type='submit' xmlns='jabber:x:data'>
- <field var='FORM_TYPE' xmlns='jabber:x:data'><value xmlns='jabber:x:data'>urn:xmpp:mam:2</value></field>
- <field var='start' xmlns='jabber:x:data'><value xmlns='jabber:x:data'>{first_timestamp}</value></field>
- <field var='end' xmlns='jabber:x:data'><value xmlns='jabber:x:data'>{second_timestamp}</value></field>
- </x>
- </query>
- </iq>"""),
-
-
- partial(expect_stanza,
- ("/message/mam:result[@queryid='qid16']/forward:forwarded/delay:delay",
- "/message/mam:result/forward:forwarded/client:message[@from='#foo%{irc_server_one}/{nick_one}'][@type='groupchat']/client:body[text()='coucou 3']")
- ),
-
- partial(expect_stanza,
- ("/message/mam:result[@queryid='qid16']/forward:forwarded/delay:delay",
- "/message/mam:result/forward:forwarded/client:message[@from='#foo%{irc_server_one}/{nick_one}'][@type='groupchat']/client:body[text()='coucou 4']")
- ),
-
- partial(expect_stanza,
- ("/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(),
- 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 two channel messages
- partial(send_stanza, "<message from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}' type='groupchat'><body>coucou</body></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]",)
- ),
-
- partial(send_stanza, "<message from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}' type='groupchat'><body>coucou 2</body></message>"),
- # Record the current time
- partial(expect_stanza, "/message[@from='#foo%{irc_server_one}/{nick_one}'][@to='{jid_one}/{resource_one}'][@type='groupchat']/body[text()='coucou 2']",
- after = partial(save_current_timestamp_plus_delta, "first_timestamp", datetime.timedelta(seconds=1))),
-
- # Wait two seconds before sending two new messages
- partial(sleep_for, 2),
- partial(send_stanza, "<message from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}' type='groupchat'><body>coucou 3</body></message>"),
- partial(send_stanza, "<message from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}' type='groupchat'><body>coucou 4</body></message>"),
- partial(expect_stanza, "/message[@type='groupchat']/body[text()='coucou 3']"),
- partial(expect_stanza, "/message[@type='groupchat']/body[text()='coucou 4']",
- after = partial(save_current_timestamp_plus_delta, "second_timestamp", datetime.timedelta(seconds=1))),
-
- # join some other channel, to stay connected to the server even after leaving #foo
- partial(send_stanza,
- "<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"),
-
- # Leave #foo
- partial(send_stanza, "<presence from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}' type='unavailable' />"),
- partial(expect_stanza, "/presence[@type='unavailable']"),
-
- # 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>"),
- partial(expect_stanza, "/message"),
- partial(expect_stanza, "/presence/muc_user:x/muc_user:status[@code='110']"),
- partial(expect_stanza, "/message/subject"),
-
- partial(send_stanza, "<presence from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}' type='unavailable' />"),
- partial(expect_stanza, "/presence[@type='unavailable']"),
-
- # 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>"),
- partial(expect_stanza, "/message"),
- partial(expect_stanza, "/presence/muc_user:x/muc_user:status[@code='110']"),
- partial(expect_stanza,
- "/message[@from='#foo%{irc_server_one}/{nick_one}'][@type='groupchat']/body[text()='coucou 2']"),
- partial(expect_stanza,
- "/message[@from='#foo%{irc_server_one}/{nick_one}'][@type='groupchat']/body[text()='coucou 3']"),
- partial(expect_stanza,
- "/message[@from='#foo%{irc_server_one}/{nick_one}'][@type='groupchat']/body[text()='coucou 4']"),
- partial(expect_stanza, "/message/subject"),
-
- partial(send_stanza, "<presence from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}' type='unavailable' />"),
- partial(expect_stanza, "/presence[@type='unavailable']"),
-
-
- # 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 since='{first_timestamp}'/></x></presence>"),
- partial(expect_stanza, "/message"),
- partial(expect_stanza, "/presence/muc_user:x/muc_user:status[@code='110']"),
- partial(expect_stanza,
- "/message[@from='#foo%{irc_server_one}/{nick_one}'][@type='groupchat']/body[text()='coucou 3']"),
- partial(expect_stanza,
- "/message[@from='#foo%{irc_server_one}/{nick_one}'][@type='groupchat']/body[text()='coucou 4']"),
- partial(expect_stanza, "/message/subject"),
- partial(send_stanza, "<presence from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}' type='unavailable' />"),
- partial(expect_stanza, "/presence[@type='unavailable']"),
-
- # 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 seconds='1'/></x></presence>"),
- partial(expect_stanza, "/message"),
- partial(expect_stanza, "/presence/muc_user:x/muc_user:status[@code='110']"),
- partial(expect_stanza,
- "/message[@from='#foo%{irc_server_one}/{nick_one}'][@type='groupchat']/body[text()='coucou 3']"),
- partial(expect_stanza,
- "/message[@from='#foo%{irc_server_one}/{nick_one}'][@type='groupchat']/body[text()='coucou 4']"),
- partial(expect_stanza, "/message/subject"),
- partial(send_stanza, "<presence from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}' type='unavailable' />"),
- partial(expect_stanza, "/presence[@type='unavailable']"),
-
- # 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 seconds='5'/></x></presence>"),
- partial(expect_stanza, "/message"),
- partial(expect_stanza, "/presence/muc_user:x/muc_user:status[@code='110']"),
- partial(expect_stanza,
- "/message[@from='#foo%{irc_server_one}/{nick_one}'][@type='groupchat']/body[text()='coucou']"), partial(expect_stanza,
- "/message[@from='#foo%{irc_server_one}/{nick_one}'][@type='groupchat']/body[text()='coucou 2']"),
- partial(expect_stanza,
- "/message[@from='#foo%{irc_server_one}/{nick_one}'][@type='groupchat']/body[text()='coucou 3']"),
- partial(expect_stanza,
- "/message[@from='#foo%{irc_server_one}/{nick_one}'][@type='groupchat']/body[text()='coucou 4']"),
- partial(expect_stanza, "/message/subject"),
- partial(send_stanza, "<presence from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}' type='unavailable' />"),
- partial(expect_stanza, "/presence[@type='unavailable']"),
-
- ]),
- Scenario("mam_on_fixed_server",
- [
- handshake_sequence(),
-
- partial(send_stanza,
- "<presence from='{jid_one}/{resource_one}' to='#foo@{biboumi_host}/{nick_one}' />"),
- connection_sequence("irc.localhost", '{jid_one}/{resource_one}', fixed_irc_server=True),
- partial(expect_stanza,
- "/message/body[text()='Mode #foo [+nt] by {irc_host_one}']"),
- partial(expect_stanza,
- ("/presence[@to='{jid_one}/{resource_one}'][@from='#foo@{biboumi_host}/{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@{biboumi_host}'][@type='groupchat']/subject[not(text())]"),
-
- partial(send_stanza, "<message from='{jid_one}/{resource_one}' to='#foo@{biboumi_host}' type='groupchat'><body>coucou</body></message>"),
- partial(expect_stanza, "/message[@from='#foo@{biboumi_host}/{nick_one}'][@to='{jid_one}/{resource_one}'][@type='groupchat']/body[text()='coucou']"),
-
- partial(send_stanza, "<message from='{jid_one}/{resource_one}' to='#foo@{biboumi_host}' type='groupchat'><body>coucou 2</body></message>"),
- partial(expect_stanza, "/message[@from='#foo@{biboumi_host}/{nick_one}'][@to='{jid_one}/{resource_one}'][@type='groupchat']/body[text()='coucou 2']"),
-
- # Retrieve the complete archive
- partial(send_stanza, "<iq to='#foo@{biboumi_host}' from='{jid_one}/{resource_one}' type='set' id='id1'><query xmlns='urn:xmpp:mam:2' queryid='qid1' /></iq>"),
-
- partial(expect_stanza,
- ("/message/mam:result[@queryid='qid1']/forward:forwarded/delay:delay",
- "/message/mam:result[@queryid='qid1']/forward:forwarded/client:message[@from='#foo@{biboumi_host}/{nick_one}'][@type='groupchat']/client:body[text()='coucou']")
- ),
- partial(expect_stanza,
- ("/message/mam:result[@queryid='qid1']/forward:forwarded/delay:delay",
- "/message/mam:result[@queryid='qid1']/forward:forwarded/client:message[@from='#foo@{biboumi_host}/{nick_one}'][@type='groupchat']/client:body[text()='coucou 2']")
- ),
- ], conf="fixed_server"),
- Scenario("default_mam_limit",
- [
- 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[@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())]",
- after = partial(save_value, "counter", lambda x: 0)),
- ] + [
- partial(send_stanza, "<message from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}' type='groupchat'><body>{counter}</body></message>"),
- partial(expect_stanza,
- "/message[@from='#foo%{irc_server_one}/{nick_one}'][@to='{jid_one}/{resource_one}'][@type='groupchat']/body[text()='{counter}']",
- after = partial(save_value, "counter", lambda stanza: str(1 + int(extract_text("/message/body", stanza))))
- ),
- ] * 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>"),
- 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()='0']")
- ),
- ] + [
- # followed by 98 more messages
- 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")
- ),
- ] * 98 + [
- # 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()='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",
- [
- handshake_sequence(),
- # First user join
- partial(send_stanza,
- "<presence from='{jid_one}/{resource_one}' to='#foo@{biboumi_host}/{nick_one}' />"),
- connection_sequence("irc.localhost", '{jid_one}/{resource_one}', fixed_irc_server=True),
- partial(expect_stanza,
- "/message/body[text()='Mode #foo [+nt] by {irc_host_one}']"),
- partial(expect_stanza,
- ("/presence[@to='{jid_one}/{resource_one}'][@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']")
- ),
- partial(expect_stanza, "/message[@from='#foo@{biboumi_host}'][@type='groupchat']/subject[not(text())]"),
-
- # Send one channel message
- partial(send_stanza, "<message from='{jid_one}/{resource_one}' to='#foo@{biboumi_host}' type='groupchat'><body>coucou</body></message>"),
- partial(expect_stanza, "/message[@from='#foo@{biboumi_host}/{nick_one}'][@to='{jid_one}/{resource_one}'][@type='groupchat']/body[text()='coucou']"),
-
- # 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']")
- ),
- # Receive the history message
- partial(expect_stanza, ("/message[@from='#foo@{biboumi_host}/{nick_one}']/body[text()='coucou']",
- "/message/delay:delay[@from='#foo@{biboumi_host}']")),
-
- partial(expect_stanza, "/message[@from='#foo@{biboumi_host}'][@type='groupchat']/subject[not(text())]"),
- ], conf="fixed_server"),
- Scenario("channel_history",
- [
- handshake_sequence(),
- # First user join
- partial(send_stanza,
- "<presence from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}/{nick_one}' />"),
- connection_sequence("irc.localhost", '{jid_one}/{resource_one}'),
- partial(expect_stanza,
- "/message/body[text()='Mode #foo [+nt] by {irc_host_one}']"),
- partial(expect_stanza,
- ("/presence[@to='{jid_one}/{resource_one}'][@from='#foo%{irc_server_one}/{nick_one}']/muc_user:x/muc_user:item[@affiliation='admin'][@jid='~nick@localhost'][@role='moderator']",
- "/presence/muc_user:x/muc_user:status[@code='110']")
- ),
- partial(expect_stanza, "/message[@from='#foo%{irc_server_one}'][@type='groupchat']/subject[not(text())]"),
-
- # Send one channel message
- partial(send_stanza, "<message from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}' type='groupchat'><body>coucou</body></message>"),
- partial(expect_stanza, "/message[@from='#foo%{irc_server_one}/{nick_one}'][@to='{jid_one}/{resource_one}'][@type='groupchat']/body[text()='coucou']"),
-
- # Second user joins
- partial(send_stanza,
- "<presence from='{jid_one}/{resource_two}' to='#foo%{irc_server_one}/{nick_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']")
- ),
- # Receive the history message
- partial(expect_stanza, ("/message[@from='#foo%{irc_server_one}/{nick_one}']/body[text()='coucou']",
- "/message/delay:delay[@from='#foo%{irc_server_one}']")),
-
- partial(expect_stanza, "/message[@from='#foo%{irc_server_one}'][@type='groupchat']/subject[not(text())]"),
- ]),
- Scenario("simple_channel_list",
- [
- 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[@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,
- "<presence from='{jid_one}/{resource_one}' to='#bar%{irc_server_one}/{nick_one}' />"),
- partial(expect_stanza,
- "/message/body[text()='Mode #bar [+nt] by {irc_host_one}']"),
- partial(expect_stanza, "/presence"),
- partial(expect_stanza, "/message[@from='#bar%{irc_server_one}'][@type='groupchat']/subject[not(text())]"),
-
- partial(send_stanza, "<iq from='{jid_one}/{resource_one}' id='id1' to='{irc_server_one}' type='get'><query xmlns='http://jabber.org/protocol/disco#items'/></iq>"),
- partial(expect_stanza, (
- "/iq[@type='result']/disco_items:query",
- "/iq/disco_items:query/disco_items:item[@jid='#foo%{irc_server_one}']",
- "/iq/disco_items:query/disco_items:item[@jid='#bar%{irc_server_one}']"
- ))
- ]),
- Scenario("channel_list_escaping",
- [
- handshake_sequence(),
-
- partial(send_stanza,
- "<presence from='{jid_one}/{resource_one}' to='#true\\2ffalse%{irc_server_one}/{nick_one}' />"),
- connection_sequence("irc.localhost", '{jid_one}/{resource_one}'),
- partial(expect_stanza,
- "/message/body[text()='Mode #true/false [+nt] by {irc_host_one}']"),
- partial(expect_stanza,
- ("/presence[@to='{jid_one}/{resource_one}'][@from='#true\\2ffalse%{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='#true\\2ffalse%{irc_server_one}'][@type='groupchat']/subject[not(text())]"),
- ]),
- Scenario("channel_list_with_rsm",
- [
- 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[@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,
- "<presence from='{jid_one}/{resource_one}' to='#bar%{irc_server_one}/{nick_one}' />"),
- partial(expect_stanza,
- "/message/body[text()='Mode #bar [+nt] by {irc_host_one}']"),
- partial(expect_stanza, "/presence"),
- partial(expect_stanza, "/message[@from='#bar%{irc_server_one}'][@type='groupchat']/subject[not(text())]"),
-
- partial(send_stanza,
- "<presence from='{jid_one}/{resource_one}' to='#coucou%{irc_server_one}/{nick_one}' />"),
- partial(expect_stanza,
- "/message/body[text()='Mode #coucou [+nt] by {irc_host_one}']"),
- partial(expect_stanza, "/presence"),
- partial(expect_stanza, "/message[@from='#coucou%{irc_server_one}'][@type='groupchat']/subject[not(text())]"),
-
- # Ask for 0 item
- partial(send_stanza, "<iq from='{jid_one}/{resource_one}' id='id1' to='{irc_server_one}' type='get'><query xmlns='http://jabber.org/protocol/disco#items'><set xmlns='http://jabber.org/protocol/rsm'><max>0</max></set></query></iq>"),
-
- # Get 0 item
- partial(expect_stanza, (
- "/iq[@type='result']/disco_items:query",
- )),
-
- # Ask for 2 (of 3) items We don’t have the count,
- # because biboumi doesn’t have the complete list when
- # it sends us the 2 items
- partial(send_stanza, "<iq from='{jid_one}/{resource_one}' id='id1' to='{irc_server_one}' type='get'><query xmlns='http://jabber.org/protocol/disco#items'><set xmlns='http://jabber.org/protocol/rsm'><max>2</max></set></query></iq>"),
- partial(expect_stanza, (
- "/iq[@type='result']/disco_items:query",
- "/iq/disco_items:query/disco_items:item[@jid='#bar%{irc_server_one}']",
- "/iq/disco_items:query/disco_items:item[@jid='#coucou%{irc_server_one}']",
- "/iq/disco_items:query/rsm:set/rsm:first[text()='#bar%{irc_server_one}'][@index='0']",
- "/iq/disco_items:query/rsm:set/rsm:last[text()='#coucou%{irc_server_one}']"
- )),
-
- # Ask for 12 (of 3) items. We get the whole list, and thus we have the count included.
- partial(send_stanza, "<iq from='{jid_one}/{resource_one}' id='id1' to='{irc_server_one}' type='get'><query xmlns='http://jabber.org/protocol/disco#items'><set xmlns='http://jabber.org/protocol/rsm'><max>12</max></set></query></iq>"),
- partial(expect_stanza, (
- "/iq[@type='result']/disco_items:query",
- "/iq/disco_items:query/disco_items:item[@jid='#bar%{irc_server_one}']",
- "/iq/disco_items:query/disco_items:item[@jid='#coucou%{irc_server_one}']",
- "/iq/disco_items:query/disco_items:item[@jid='#foo%{irc_server_one}']",
- "/iq/disco_items:query/rsm:set/rsm:first[text()='#bar%{irc_server_one}'][@index='0']",
- "/iq/disco_items:query/rsm:set/rsm:last[text()='#foo%{irc_server_one}']",
- "/iq/disco_items:query/rsm:set/rsm:count[text()='3']"
- )),
-
- # Ask for 1 item, AFTER the first item (so,
- # the second). Since we don’t invalidate the cache
- # with this request, we should have the count
- # included.
- partial(send_stanza, "<iq from='{jid_one}/{resource_one}' id='id1' to='{irc_server_one}' type='get'><query xmlns='http://jabber.org/protocol/disco#items'><set xmlns='http://jabber.org/protocol/rsm'><after>#bar%{irc_server_one}</after><max>1</max></set></query></iq>"),
- partial(expect_stanza, (
- "/iq[@type='result']/disco_items:query",
- "/iq/disco_items:query/disco_items:item[@jid='#coucou%{irc_server_one}']",
- "/iq/disco_items:query/rsm:set/rsm:first[text()='#coucou%{irc_server_one}'][@index='1']",
- "/iq/disco_items:query/rsm:set/rsm:last[text()='#coucou%{irc_server_one}']",
- "/iq/disco_items:query/rsm:set/rsm:count[text()='3']"
- )),
-
- # Ask for 1 item, AFTER the second item (so,
- # the third).
- partial(send_stanza, "<iq from='{jid_one}/{resource_one}' id='id1' to='{irc_server_one}' type='get'><query xmlns='http://jabber.org/protocol/disco#items'><set xmlns='http://jabber.org/protocol/rsm'><after>#coucou%{irc_server_one}</after><max>1</max></set></query></iq>"),
- partial(expect_stanza, (
- "/iq[@type='result']/disco_items:query",
- "/iq/disco_items:query/disco_items:item[@jid='#foo%{irc_server_one}']",
- "/iq/disco_items:query/rsm:set/rsm:first[text()='#foo%{irc_server_one}'][@index='2']",
- "/iq/disco_items:query/rsm:set/rsm:last[text()='#foo%{irc_server_one}']",
- "/iq/disco_items:query/rsm:set/rsm:count[text()='3']"
- )),
-
- # Ask for 1 item, AFTER the third item (so,
- # the fourth). Since it doesn't exist, we get 0 item
- partial(send_stanza, "<iq from='{jid_one}/{resource_one}' id='id1' to='{irc_server_one}' type='get'><query xmlns='http://jabber.org/protocol/disco#items'><set xmlns='http://jabber.org/protocol/rsm'><after>#foo%{irc_server_one}</after><max>1</max></set></query></iq>"),
- partial(expect_stanza, (
- "/iq[@type='result']/disco_items:query",
- "/iq/disco_items:query/rsm:set/rsm:count[text()='3']"
- )),
- ]),
- Scenario("default_channel_list_limit",
- [
- 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"),
- partial(expect_stanza, "/presence"),
- partial(expect_stanza, "/message",
- after = partial(save_value, "counter", lambda x: 0)),
- ] + [
- partial(send_stanza,
- "<presence from='{jid_one}/{resource_one}' to='#{counter}%{irc_server_one}/{nick_one}' />"),
- partial(expect_stanza, "/message"),
- partial(expect_stanza, "/presence",
- after = partial(save_value, "counter", lambda stanza: str(1 + int(chan_name_from_jid(extract_attribute("/presence", "from", stanza)))))),
- partial(expect_stanza, "/message")
- ] * 110 + [
- partial(send_stanza, "<iq from='{jid_one}/{resource_one}' id='id1' to='{irc_server_one}' type='get'><query xmlns='http://jabber.org/protocol/disco#items'/></iq>"),
- # charybdis sends the list in alphabetic order, so #foo is the last, and #99 is after #120
- partial(expect_stanza, ("/iq/disco_items:query/disco_items:item[@jid='#0%{irc_server_one}']",
- "/iq/disco_items:query/disco_items:item[@jid='#1%{irc_server_one}']",
- "/iq/disco_items:query/disco_items:item[@jid='#109%{irc_server_one}']",
- "/iq/disco_items:query/disco_items:item[@jid='#9%{irc_server_one}']",
- "!/iq/disco_items:query/disco_items:item[@jid='#foo%{irc_server_one}']",
- "!/iq/disco_items:query/disco_items:item[@jid='#99%{irc_server_one}']",
- "!/iq/disco_items:query/disco_items:item[@jid='#90%{irc_server_one}']")),
- ]),
- Scenario("complete_channel_list_with_pages_of_3",
- [
- handshake_sequence(),
-
- partial(send_stanza,
- "<presence from='{jid_one}/{resource_one}' to='#aaa%{irc_server_one}/{nick_one}' />"),
- connection_sequence("irc.localhost", '{jid_one}/{resource_one}'),
- partial(expect_stanza, "/message"),
- partial(expect_stanza, "/presence"),
- partial(expect_stanza, "/message"),
-
- partial(send_stanza,
- "<presence from='{jid_one}/{resource_one}' to='#bbb%{irc_server_one}/{nick_one}' />"),
- partial(expect_stanza, "/message"),
- partial(expect_stanza, "/presence"),
- partial(expect_stanza, "/message"),
-
- partial(send_stanza,
- "<presence from='{jid_one}/{resource_one}' to='#ccc%{irc_server_one}/{nick_one}' />"),
- partial(expect_stanza, "/message"),
- partial(expect_stanza, "/presence"),
- partial(expect_stanza, "/message"),
-
- partial(send_stanza,
- "<presence from='{jid_one}/{resource_one}' to='#ddd%{irc_server_one}/{nick_one}' />"),
- partial(expect_stanza, "/message"),
- partial(expect_stanza, "/presence"),
- partial(expect_stanza, "/message"),
-
- partial(send_stanza,
- "<presence from='{jid_one}/{resource_one}' to='#eee%{irc_server_one}/{nick_one}' />"),
- partial(expect_stanza, "/message"),
- partial(expect_stanza, "/presence"),
- partial(expect_stanza, "/message"),
-
- partial(send_stanza,
- "<presence from='{jid_one}/{resource_one}' to='#fff%{irc_server_one}/{nick_one}' />"),
- partial(expect_stanza, "/message"),
- partial(expect_stanza, "/presence"),
- partial(expect_stanza, "/message"),
-
- partial(send_stanza,
- "<presence from='{jid_one}/{resource_one}' to='#ggg%{irc_server_one}/{nick_one}' />"),
- partial(expect_stanza, "/message"),
- partial(expect_stanza, "/presence"),
- partial(expect_stanza, "/message"),
-
- partial(send_stanza,
- "<presence from='{jid_one}/{resource_one}' to='#hhh%{irc_server_one}/{nick_one}' />"),
- partial(expect_stanza, "/message"),
- partial(expect_stanza, "/presence"),
- partial(expect_stanza, "/message"),
-
- partial(send_stanza,
- "<presence from='{jid_one}/{resource_one}' to='#iii%{irc_server_one}/{nick_one}' />"),
- partial(expect_stanza, "/message"),
- partial(expect_stanza, "/presence"),
- partial(expect_stanza, "/message"),
-
- partial(send_stanza,
- "<presence from='{jid_one}/{resource_one}' to='#jjj%{irc_server_one}/{nick_one}' />"),
- partial(expect_stanza, "/message"),
- partial(expect_stanza, "/presence"),
- partial(expect_stanza, "/message"),
-
- partial(send_stanza, "<iq from='{jid_one}/{resource_one}' id='id' to='{irc_server_one}' type='get'><query xmlns='http://jabber.org/protocol/disco#items'><set xmlns='http://jabber.org/protocol/rsm'><max>3</max></set></query></iq>"),
- partial(expect_stanza, (
- "/iq[@type='result']/disco_items:query",
- "/iq/disco_items:query/disco_items:item[@jid='#aaa%{irc_server_one}']",
- "/iq/disco_items:query/disco_items:item[@jid='#bbb%{irc_server_one}']",
- "/iq/disco_items:query/disco_items:item[@jid='#ccc%{irc_server_one}']",
- "/iq/disco_items:query/rsm:set/rsm:first[text()='#aaa%{irc_server_one}'][@index='0']",
- "/iq/disco_items:query/rsm:set/rsm:last[text()='#ccc%{irc_server_one}']"
- )),
-
- partial(send_stanza, "<iq from='{jid_one}/{resource_one}' id='id' to='{irc_server_one}' type='get'><query xmlns='http://jabber.org/protocol/disco#items'><set xmlns='http://jabber.org/protocol/rsm'><after>#ccc%{irc_server_one}</after><max>3</max></set></query></iq>"),
- partial(expect_stanza, (
- "/iq[@type='result']/disco_items:query",
- "/iq/disco_items:query/disco_items:item[@jid='#ddd%{irc_server_one}']",
- "/iq/disco_items:query/disco_items:item[@jid='#eee%{irc_server_one}']",
- "/iq/disco_items:query/disco_items:item[@jid='#fff%{irc_server_one}']",
- "/iq/disco_items:query/rsm:set/rsm:first[text()='#ddd%{irc_server_one}'][@index='3']",
- "/iq/disco_items:query/rsm:set/rsm:last[text()='#fff%{irc_server_one}']"
- )),
-
- partial(send_stanza, "<iq from='{jid_one}/{resource_one}' id='id' to='{irc_server_one}' type='get'><query xmlns='http://jabber.org/protocol/disco#items'><set xmlns='http://jabber.org/protocol/rsm'><after>#fff%{irc_server_one}</after><max>3</max></set></query></iq>"),
- partial(expect_stanza, (
- "/iq[@type='result']/disco_items:query",
- "/iq/disco_items:query/disco_items:item[@jid='#ggg%{irc_server_one}']",
- "/iq/disco_items:query/disco_items:item[@jid='#hhh%{irc_server_one}']",
- "/iq/disco_items:query/disco_items:item[@jid='#iii%{irc_server_one}']",
- "/iq/disco_items:query/rsm:set/rsm:first[text()='#ggg%{irc_server_one}'][@index='6']",
- "/iq/disco_items:query/rsm:set/rsm:last[text()='#iii%{irc_server_one}']"
- )),
-
- partial(send_stanza, "<iq from='{jid_one}/{resource_one}' id='id' to='{irc_server_one}' type='get'><query xmlns='http://jabber.org/protocol/disco#items'><set xmlns='http://jabber.org/protocol/rsm'><after>#iii%{irc_server_one}</after><max>3</max></set></query></iq>"),
- partial(expect_stanza, (
- "/iq[@type='result']/disco_items:query",
- "/iq/disco_items:query/disco_items:item[@jid='#jjj%{irc_server_one}']",
- "/iq/disco_items:query/rsm:set/rsm:first[text()='#jjj%{irc_server_one}'][@index='9']",
- "/iq/disco_items:query/rsm:set/rsm:last[text()='#jjj%{irc_server_one}']",
- "/iq/disco_items:query/rsm:set/rsm:count[text()='10']"
- )),
-
- partial(send_stanza, "<presence from='{jid_one}/{resource_one}' to='#aaa%{irc_server_one}/{nick_one}' type='unavailable' />"),
- partial(send_stanza, "<presence from='{jid_one}/{resource_one}' to='#bbb%{irc_server_one}/{nick_one}' type='unavailable' />"),
- partial(send_stanza, "<presence from='{jid_one}/{resource_one}' to='#ccc%{irc_server_one}/{nick_one}' type='unavailable' />"),
- partial(send_stanza, "<presence from='{jid_one}/{resource_one}' to='#ddd%{irc_server_one}/{nick_one}' type='unavailable' />"),
- partial(send_stanza, "<presence from='{jid_one}/{resource_one}' to='#eee%{irc_server_one}/{nick_one}' type='unavailable' />"),
- partial(send_stanza, "<presence from='{jid_one}/{resource_one}' to='#fff%{irc_server_one}/{nick_one}' type='unavailable' />"),
- partial(send_stanza, "<presence from='{jid_one}/{resource_one}' to='#ggg%{irc_server_one}/{nick_one}' type='unavailable' />"),
- partial(send_stanza, "<presence from='{jid_one}/{resource_one}' to='#hhh%{irc_server_one}/{nick_one}' type='unavailable' />"),
- partial(send_stanza, "<presence from='{jid_one}/{resource_one}' to='#iii%{irc_server_one}/{nick_one}' type='unavailable' />"),
- partial(send_stanza, "<presence from='{jid_one}/{resource_one}' to='#jjj%{irc_server_one}/{nick_one}' type='unavailable' />"),
- partial(expect_stanza, "/presence[@type='unavailable']"),
- partial(expect_stanza, "/presence[@type='unavailable']"),
- partial(expect_stanza, "/presence[@type='unavailable']"),
- partial(expect_stanza, "/presence[@type='unavailable']"),
- partial(expect_stanza, "/presence[@type='unavailable']"),
- partial(expect_stanza, "/presence[@type='unavailable']"),
- partial(expect_stanza, "/presence[@type='unavailable']"),
- partial(expect_stanza, "/presence[@type='unavailable']"),
- partial(expect_stanza, "/presence[@type='unavailable']"),
- partial(expect_stanza, "/presence[@type='unavailable']")
- ]),
- Scenario("muc_traffic_info",
- [
- handshake_sequence(),
-
- partial(send_stanza,
- "<iq from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}' id='1' type='get'><query xmlns='http://jabber.org/protocol/disco#info' node='http://jabber.org/protocol/muc#traffic'/></iq>"),
- partial(expect_stanza, "/iq[@from='#foo%{irc_server_one}'][@to='{jid_one}/{resource_one}'][@type='result']/disco_info:query[@node='http://jabber.org/protocol/muc#traffic']"),
- ]),
- Scenario("muc_disco_info",
- [
- handshake_sequence(),
-
- partial(send_stanza,
- "<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='#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']",
- )),
- ]),
- Scenario("fixed_muc_disco_info",
- [
- handshake_sequence(),
-
- partial(send_stanza,
- "<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='#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']",
- )),
- ], conf='fixed_server'),
- Scenario("raw_message",
- [
- 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"),
- partial(expect_stanza, "/presence"),
- partial(expect_stanza, "/message"),
-
- partial(send_stanza, "<message from='{jid_one}/{resource_one}' to='{irc_server_one}' type='chat'><body>WHOIS {nick_one}</body></message>"),
- partial(expect_stanza, "/message[@from='{irc_server_one}'][@type='chat']/body[text()='irc.localhost: {nick_one} ~{nick_one} localhost * {nick_one}']"),
- ]),
- Scenario("raw_message_fixed_irc_server",
- [
- 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}', fixed_irc_server=True),
- partial(expect_stanza, "/message"),
- partial(expect_stanza, "/presence"),
- partial(expect_stanza, "/message"),
-
- partial(send_stanza, "<message from='{jid_one}/{resource_one}' to='{biboumi_host}' type='chat'><body>WHOIS {nick_one}</body></message>"),
- partial(expect_stanza, "/message[@from='{biboumi_host}'][@type='chat']/body[text()='irc.localhost: {nick_one} ~{nick_one} localhost * {nick_one}']"),
- ], conf='fixed_server'),
- Scenario("self_disco_info",
- [
- handshake_sequence(),
- partial(send_stanza, "<iq type='get' id='get1' from='{jid_one}/{resource_one}' to='{biboumi_host}'><query xmlns='http://jabber.org/protocol/disco#info'/></iq>"),
- partial(expect_stanza,
- ("/iq[@type='result']/disco_info:query/disco_info:identity[@category='conference'][@type='irc'][@name='Biboumi XMPP-IRC gateway']",
- "/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']",
- )),
- ]),
- Scenario("invite_other",
- [
- 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"),
- partial(expect_stanza, "/presence"),
- partial(expect_stanza, "/message"),
-
- partial(send_stanza, "<presence from='{jid_two}/{resource_two}' to='#bar%{irc_server_one}@{biboumi_host}/{nick_two}' />"),
- connection_sequence("irc.localhost", '{jid_two}/{resource_two}'),
- partial(expect_stanza, "/message"),
- partial(expect_stanza, "/presence"),
- partial(expect_stanza, "/message"),
- partial(send_stanza, "<message from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}'><x xmlns='http://jabber.org/protocol/muc#user'><invite to='{nick_two}'/></x></message>"),
- partial(expect_stanza, "/message/body[text()='{nick_two} has been invited to #foo']"),
- partial(expect_stanza, "/message[@to='{jid_two}/{resource_two}'][@from='#foo%{irc_server_one}']/muc_user:x/muc_user:invite[@from='#foo%{irc_server_one}/{nick_one}']"),
-
- 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("global_configure",
- [
- 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='configure' action='execute' /></iq>"),
- partial(expect_stanza, ("/iq[@type='result']/commands:command[@node='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='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='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='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='configure' action='execute' /></iq>"),
- partial(expect_stanza, ("/iq[@type='result']/commands:command[@node='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='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='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(),
- partial(send_stanza, "<iq type='set' id='id1' from='{jid_one}/{resource_one}' to='{biboumi_host}'><command xmlns='http://jabber.org/protocol/commands' node='configure' action='execute' /></iq>"),
- partial(expect_stanza, ("/iq[@type='result']/commands:command[@node='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()='true']",
- "/iq/commands:command/commands:actions/commands:next",
- ),
- ),
- ],conf='persistent_by_default'),
- Scenario("irc_server_configure",
- [
- handshake_sequence(),
- 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']/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[@type='form']/dataform:field[@type='text-multi'][@var='ports']/dataform:value[text()='6667']",
- "/iq/commands:command/dataform:x[@type='form']/dataform:field[@type='text-multi'][@var='tls_ports']/dataform:value[text()='6670']",
- "/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-private'][@var='pass']",
- "/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']",
- "/iq/commands:command/dataform:x[@type='form']/dataform:field[@type='text-single'][@var='encoding_out']",
- "/iq/commands:command/commands:actions/commands:next",
- ),
- 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' />"
- "<field var='tls_ports'><value>6697</value><value>6698</value></field>"
- "<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_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='realname'><value>realname</value></field>"
- "<field var='encoding_out'><value>UTF-8</value></field>"
- "<field var='encoding_in'><value>latin-1</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, "<iq type='set' id='id3' 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']/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[@type='form']/dataform:field[@type='text-multi']",
- "/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='text-multi'][@var='tls_ports']/dataform:value[text()='6698']",
- "/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='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='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",
- ),
- after = partial(save_value, "sessionid", partial(extract_attribute, "/iq[@type='result']/commands:command[@node='configure']", "sessionid"))
- ),
- partial(send_stanza, "<iq type='set' id='id4' from='{jid_one}/{resource_one}' to='{irc_server_one}'><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']"),
-
- # Same thing, but try to empty some values
- 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='pass'><value></value></field>"
- "<field var='after_connect_commands'></field>"
- "<field var='username'><value></value></field>"
- "<field var='realname'><value></value></field>"
- "<field var='encoding_out'><value></value></field>"
- "<field var='encoding_in'><value></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, "<iq type='set' id='id3' 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']/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_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",
- ),
- after = partial(save_value, "sessionid", partial(extract_attribute, "/iq[@type='result']/commands:command[@node='configure']", "sessionid"))
- ),
- partial(send_stanza, "<iq type='set' id='id4' from='{jid_one}/{resource_one}' to='{irc_server_one}'><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("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(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']",
- ),
- 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='#foo%{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' />"
- "<field var='encoding_out'><value>UTF-8</value></field>"
- "<field var='encoding_in'><value>latin-1</value></field>"
- "<field var='record_history'><value>true</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, "<iq type='set' id='id3' from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}'><command xmlns='http://jabber.org/protocol/commands' node='configure' action='execute' /></iq>"),
- 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 channel #foo on server irc.localhost']",
- "/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/dataform:x[@type='form']/dataform:field[@type='list-single'][@var='record_history']/dataform:value[text()='true']",
- "/iq/commands:command/commands:actions/commands:next",
- ),
- after = partial(save_value, "sessionid", partial(extract_attribute, "/iq[@type='result']/commands:command[@node='configure']", "sessionid"))
- ),
- partial(send_stanza, "<iq type='set' id='id4' from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}'><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("irc_channel_configure_xep0045",
- [
- handshake_sequence(),
- partial(send_stanza, "<iq type='get' id='id1' from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}'><query xmlns='http://jabber.org/protocol/muc#owner'/></iq>"),
- partial(expect_stanza, ("/iq[@type='result']/muc_owner:query",
- "/iq/muc_owner:query/dataform:x[@type='form']/dataform:field[@type='text-single'][@var='encoding_in']",
- "/iq/muc_owner:query/dataform:x[@type='form']/dataform:field[@type='text-single'][@var='encoding_out']",
- ),
- ),
- partial(send_stanza, "<iq type='set' id='id2' from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}'>"
- "<query xmlns='http://jabber.org/protocol/muc#owner'>"
- "<x xmlns='jabber:x:data' type='submit'>"
- "<field var='ports' />"
- "<field var='encoding_out'><value>UTF-8</value></field>"
- "<field var='encoding_in'><value>latin-1</value></field>"
- "</x></query></iq>"),
- partial(expect_stanza, "/iq[@type='result']"),
- partial(send_stanza, "<iq type='set' id='id3' from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}'><query xmlns='http://jabber.org/protocol/muc#owner'> <x xmlns='jabber:x:data' type='cancel'/></query></iq>"),
- partial(expect_stanza, "/iq[@type='result']"),
- ]),
- Scenario("irc_channel_configure_fixed",
- [
- handshake_sequence(),
- partial(send_stanza, "<iq type='set' id='id1' from='{jid_one}/{resource_one}' to='#foo@{biboumi_host}'><command xmlns='http://jabber.org/protocol/commands' node='configure' action='execute' /></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']",
- ),
- 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='#foo@{biboumi_host}'>"
- "<command xmlns='http://jabber.org/protocol/commands' node='configure' sessionid='{sessionid}' action='next'>"
- "<x xmlns='jabber:x:data' type='submit'>"
- "<field var='ports' />"
- "<field var='encoding_out'><value>UTF-8</value></field>"
- "<field var='encoding_in'><value>latin-1</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, "<iq type='set' id='id3' from='{jid_one}/{resource_one}' to='#foo@{biboumi_host}'><command xmlns='http://jabber.org/protocol/commands' node='configure' action='execute' /></iq>"),
- 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 channel #foo on server irc.localhost']",
- "/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",
- ),
- after = partial(save_value, "sessionid", partial(extract_attribute, "/iq[@type='result']/commands:command[@node='configure']", "sessionid"))
- ),
- partial(send_stanza, "<iq type='set' id='id4' from='{jid_one}/{resource_one}' to='#foo@{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']"),
- ], conf='fixed_server'),
- Scenario("irc_tls_connection",
- [
- handshake_sequence(),
- # First, use an adhoc command to configure how we connect to the irc server, configure
- # only one TLS port, and disable the cert verification.
- 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' />"
- "<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.']"),
-
- partial(send_stanza,
- "<presence from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}/{nick_one}' />"),
- connection_tls_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}/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())]"),
- ]),
- Scenario("get_irc_connection_info",
- [
- handshake_sequence(),
-
- partial(send_stanza, "<iq type='set' id='command1' from='{jid_one}/{resource_one}' to='{irc_server_one}'><command xmlns='http://jabber.org/protocol/commands' node='get-irc-connection-info' action='execute' /></iq>"),
- partial(expect_stanza, "/iq/commands:command/commands:note[text()='You are not connected to the IRC server irc.localhost']"),
-
- 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"),
- partial(expect_stanza, "/presence"),
- partial(expect_stanza, "/message"),
-
- partial(send_stanza, "<iq type='set' id='command2' from='{jid_one}/{resource_one}' to='{irc_server_one}'><command xmlns='http://jabber.org/protocol/commands' node='get-irc-connection-info' action='execute' /></iq>"),
- partial(expect_stanza, r"/iq/commands:command/commands:note[re:test(text(), 'Connected to IRC server irc.localhost on port 6667 since \d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d \(\d+ seconds ago\)\.\n#foo from 1 resource: {resource_one}.*')]"),
- ]),
- Scenario("get_irc_connection_info_fixed",
- [
- handshake_sequence(),
-
- partial(send_stanza, "<iq type='set' id='command1' from='{jid_one}/{resource_one}' to='{biboumi_host}'><command xmlns='http://jabber.org/protocol/commands' node='get-irc-connection-info' action='execute' /></iq>"),
- partial(expect_stanza, "/iq/commands:command/commands:note[text()='You are not connected to the IRC server irc.localhost']"),
-
- partial(send_stanza,
- "<presence from='{jid_one}/{resource_one}' to='#foo@{biboumi_host}/{nick_one}' />"),
- connection_sequence("irc.localhost", '{jid_one}/{resource_one}', fixed_irc_server=True),
- partial(expect_stanza, "/message"),
- partial(expect_stanza, "/presence"),
- partial(expect_stanza, "/message"),
-
- partial(send_stanza, "<iq type='set' id='command2' from='{jid_one}/{resource_one}' to='{biboumi_host}'><command xmlns='http://jabber.org/protocol/commands' node='get-irc-connection-info' action='execute' /></iq>"),
- partial(expect_stanza, r"/iq/commands:command/commands:note[re:test(text(), 'Connected to IRC server irc.localhost on port 6667 since \d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d \(\d+ seconds ago\)\.\n#foo from 1 resource: {resource_one}.*')]"),
- ], conf='fixed_server'),
- Scenario("irc_server_presence_subscription",
- [
- handshake_sequence(),
- partial(send_stanza, "<presence type='subscribe' from='{jid_one}/{resource_one}' to='{irc_server_one}' id='sub1' />"),
- partial(expect_stanza, "/presence[@to='{jid_one}'][@from='{irc_server_one}'][@type='subscribed']")
- ]),
- Scenario("fixed_irc_server_presence_subscription",
- [
- handshake_sequence(),
- partial(send_stanza, "<presence type='subscribe' from='{jid_one}/{resource_one}' to='{biboumi_host}' id='sub1' />"),
- partial(expect_stanza, "/presence[@to='{jid_one}'][@from='{biboumi_host}'][@type='subscribed']")
- ], conf='fixed_server'),
- Scenario("leave_unjoined_chan",
- [
- 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"),
- partial(expect_stanza, "/presence"),
- partial(expect_stanza, "/message"),
-
- partial(send_stanza, "<presence from='{jid_two}/{resource_two}' to='#foo%{irc_server_one}/{nick_one}' />"),
- connection_begin_sequence("irc.localhost", '{jid_two}/{resource_two}'),
-
- partial(expect_stanza, "/message[@to='{jid_two}/{resource_two}'][@type='chat']/body[text()='irc.localhost: {nick_one}: Nickname is already in use.']"),
- partial(expect_stanza, "/presence[@type='error']/error[@type='cancel'][@code='409']/stanza:conflict"),
- partial(send_stanza, "<presence from='{jid_two}/{resource_two}' to='#foo%{irc_server_one}/{nick_one}' type='unavailable' />")
- ]),
- Scenario("basic_subscribe_unsubscribe",
- [
- handshake_sequence(),
-
- # Mutual subscription exchange
- partial(send_stanza, "<presence from='{jid_one}' to='{biboumi_host}' type='subscribe' id='subid1' />"),
- partial(expect_stanza, "/presence[@type='subscribed'][@id='subid1']"),
-
- # Get the current presence of the biboumi gateway
- partial(expect_stanza, "/presence"),
-
- partial(expect_stanza, "/presence[@type='subscribe']"),
- partial(send_stanza, "<presence from='{jid_one}' to='{biboumi_host}' type='subscribed' />"),
-
-
- # Unsubscribe
- partial(send_stanza, "<presence from='{jid_one}' to='{biboumi_host}' type='unsubscribe' id='unsubid1' />"),
- partial(expect_stanza, "/presence[@type='unavailable']"),
- partial(expect_stanza, "/presence[@type='unsubscribed']"),
- partial(expect_stanza, "/presence[@type='unsubscribe']"),
- partial(send_stanza, "<presence from='{jid_one}' to='{biboumi_host}' type='unavailable' />"),
- partial(send_stanza, "<presence from='{jid_one}' to='{biboumi_host}' type='unsubscribed' />"),
- ]),
- Scenario("resource_is_removed_from_server_when_last_chan_is_left",
- [
- # Join the channel
- 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[@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'][@to='{jid_one}/{resource_one}']/subject[not(text())]"),
-
- # Make it persistent
- partial(send_stanza, "<iq from='{jid_one}/{resource_one}' id='conf1' to='#foo%{irc_server_one}' type='get'><query xmlns='http://jabber.org/protocol/muc#owner'/></iq>"),
- partial(expect_stanza, "/iq[@type='result']/muc_owner:query/dataform:x/dataform:field[@var='persistent'][@type='boolean']/dataform:value[text()='false']"),
- partial(send_stanza, "<iq from='{jid_one}/{resource_one}' id='conf2' to='#foo%{irc_server_one}' type='set'><query xmlns='http://jabber.org/protocol/muc#owner'><x type='submit' xmlns='jabber:x:data'><field var='persistent' xmlns='jabber:x:data'><value>true</value></field></x></query></iq>"),
- partial(expect_stanza, "/iq[@type='result']"),
-
- partial(send_stanza, "<presence from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}/{nick_one}' type='unavailable' />"),
- partial(expect_stanza, "/presence[@type='unavailable'][@from='#foo%{irc_server_one}/{nick_one}']"),
-
- # Join the same channel, with the same JID, but a different resource
- partial(send_stanza,
- "<presence from='{jid_one}/{resource_two}' to='#foo%{irc_server_one}/{nick_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'][@role='moderator']",
- "/presence/muc_user:x/muc_user:status[@code='110']")
- ),
- partial(expect_stanza, "/message[@from='#foo%{irc_server_one}'][@type='groupchat'][@to='{jid_one}/{resource_two}']/subject[not(text())]"),
-
- # Join some other channel with someone else
- partial(send_stanza,
- "<presence from='{jid_two}/{resource_one}' to='#bar%{irc_server_one}/{nick_two}' />"),
- connection_sequence("irc.localhost", '{jid_two}/{resource_one}'),
- partial(expect_stanza,
- "/message/body[text()='Mode #bar [+nt] by {irc_host_one}']"),
- partial(expect_stanza,
- ("/presence[@to='{jid_two}/{resource_one}'][@from='#bar%{irc_server_one}/{nick_two}']/muc_user:x/muc_user:item[@affiliation='admin'][@role='moderator']",
- "/presence/muc_user:x/muc_user:status[@code='110']")
- ),
- partial(expect_stanza, "/message[@from='#bar%{irc_server_one}'][@type='groupchat'][@to='{jid_two}/{resource_one}']/subject[not(text())]"),
-
- # Send two messages from the second user to the first one
- partial(send_stanza, "<message from='{jid_two}/{resource_one}' to='{lower_nick_one}%{irc_server_one}' type='chat'><body>kikoo</body></message>"),
- partial(send_stanza, "<message from='{jid_two}/{resource_one}' to='{lower_nick_one}%{irc_server_one}' type='chat'><body>second kikoo</body></message>"),
-
- # We must receive each message only once, no duplicate
- partial(expect_stanza, "/message/body[text()='kikoo']"),
- partial(expect_stanza, "/message/body[text()='second kikoo']"),
- ]
- ),
- Scenario("irc_server_presence_in_roster",
- [
- handshake_sequence(),
-
- # Mutual subscription exchange
- partial(send_stanza, "<presence from='{jid_one}' to='{irc_server_one}' type='subscribe' id='subid1' />"),
- partial(expect_stanza, "/presence[@type='subscribed'][@id='subid1']"),
-
- partial(expect_stanza, "/presence[@type='subscribe']"),
- partial(send_stanza, "<presence from='{jid_one}' to='{irc_server_one}' type='subscribed' />"),
-
- # Join a channel on that server
- partial(send_stanza,
- "<presence from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}/{nick_one}' />"),
-
- # We must receive the IRC server presence, in the connection sequence
- connection_sequence("irc.localhost", '{jid_one}/{resource_one}', expected_irc_presence=True),
- 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())]"),
-
- # Leave the channel, and thus the IRC server
- partial(send_stanza, "<presence type='unavailable' from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}/{nick_one}' />"),
- partial(expect_stanza, "/presence[@type='unavailable'][@from='#foo%{irc_server_one}/{nick_one}']"),
- partial(expect_stanza, "/presence[@from='{irc_server_one}'][@to='{jid_one}'][@type='unavailable']"),
- ])
- )
-
- failures = 0
-
- scenar_list = sys.argv[1:]
irc_output = open("irc_output.txt", "w")
irc = IrcServerRunner()
print("Starting irc server…")
@@ -3249,17 +329,18 @@ if __name__ == '__main__':
if b"now running in foreground mode" in res:
break
print("irc server started.")
- checks = len([s for s in scenarios if s.name in scenar_list]) if scenar_list else len(scenarios)
- print("Running %s checks for biboumi." % checks)
+ number_of_checks = len([s for s in scenarios if s.name in provided_scenar_names]) if provided_scenar_names else len(scenarios)
+ print("Running %s checks for biboumi." % number_of_checks)
+
+ failures = 0
for s in scenarios:
- if scenar_list and s.name not in scenar_list:
- continue
test = BiboumiTest(s)
if not test.run():
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()
@@ -3271,3 +352,4 @@ if __name__ == '__main__':
sys.exit(1)
else:
print("All tests passed successfully")
+
diff --git a/tests/end_to_end/functions.py b/tests/end_to_end/functions.py
new file mode 100644
index 0000000..97cdfb0
--- /dev/null
+++ b/tests/end_to_end/functions.py
@@ -0,0 +1,169 @@
+from functools import partial
+import collections
+import datetime
+import asyncio
+import time
+import lxml.etree
+import io
+
+common_replacements = {
+ 'irc_server_one': 'irc.localhost@biboumi.localhost',
+ 'irc_server_two': 'localhost@biboumi.localhost',
+ 'irc_host_one': 'irc.localhost',
+ 'irc_host_two': 'localhost',
+ 'biboumi_host': 'biboumi.localhost',
+ 'resource_one': 'resource1',
+ 'resource_two': 'resource2',
+ 'nick_one': 'Nick',
+ 'jid_one': 'first@example.com',
+ 'jid_two': 'second@example.com',
+ 'jid_admin': 'admin@example.com',
+ 'nick_two': 'Bobby',
+ 'nick_three': 'Bernard',
+ 'lower_nick_one': 'nick',
+ 'lower_nick_two': 'bobby',
+}
+
+class SkipStepError(Exception):
+ """
+ Raised by a step when it needs to be skiped, by running
+ the next available step immediately.
+ """
+ pass
+
+class StanzaError(Exception):
+ """
+ Raised when a step fails.
+ """
+ pass
+
+def match(stanza, xpath):
+ tree = lxml.etree.parse(io.StringIO(str(stanza)))
+ matched = tree.xpath(xpath, namespaces={'re': 'http://exslt.org/regular-expressions',
+ 'muc_user': 'http://jabber.org/protocol/muc#user',
+ 'muc_owner': 'http://jabber.org/protocol/muc#owner',
+ 'muc': 'http://jabber.org/protocol/muc',
+ 'disco_info': 'http://jabber.org/protocol/disco#info',
+ 'muc_traffic': 'http://jabber.org/protocol/muc#traffic',
+ 'disco_items': 'http://jabber.org/protocol/disco#items',
+ 'commands': 'http://jabber.org/protocol/commands',
+ 'dataform': 'jabber:x:data',
+ 'version': 'jabber:iq:version',
+ 'mam': 'urn:xmpp:mam:2',
+ 'rms': 'http://jabber.org/protocol/rsm',
+ 'delay': 'urn:xmpp:delay',
+ 'forward': 'urn:xmpp:forward:0',
+ 'client': 'jabber:client',
+ 'rsm': 'http://jabber.org/protocol/rsm',
+ 'carbon': 'urn:xmpp:carbons:2',
+ 'hints': 'urn:xmpp:hints',
+ 'stanza': 'urn:ietf:params:xml:ns:xmpp-stanzas',
+ 'stable_id': 'urn:xmpp:sid:0'})
+ return matched
+
+def check_xpath(xpaths, xmpp, after, stanza):
+ for xpath in xpaths:
+ expected = True
+ real_xpath = xpath
+ # We can check that a stanza DOESN’T match, by adding a ! before it.
+ if xpath.startswith('!'):
+ expected = False
+ xpath = xpath[1:]
+ matched = match(stanza, xpath)
+ if (expected and not matched) or (not expected and matched):
+ 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:
+ af(stanza, xmpp)
+ else:
+ after(stanza, xmpp)
+
+def check_xpath_optional(xpaths, xmpp, after, stanza):
+ try:
+ check_xpath(xpaths, xmpp, after, stanza)
+ except StanzaError:
+ raise SkipStepError()
+
+def all_xpaths_match(stanza, xpaths):
+ try:
+ check_xpath(xpaths, None, None, stanza)
+ except StanzaError:
+ return False
+ return True
+
+def check_list_of_xpath(list_of_xpaths, xmpp, stanza):
+ found = False
+ for i, xpaths in enumerate(list_of_xpaths):
+ if all_xpaths_match(stanza, xpaths):
+ found = True
+ list_of_xpaths.pop(i)
+ break
+
+ if not found:
+ raise StanzaError("Received stanza “%s” did not match any of the expected xpaths:\n%s" % (stanza, list_of_xpaths))
+
+ if list_of_xpaths:
+ step = partial(expect_unordered_already_formatted, list_of_xpaths)
+ xmpp.scenario.steps.insert(0, step)
+
+def extract_attribute(xpath, name):
+ def f(xpath, name, stanza):
+ matched = match(stanza, xpath)
+ return matched[0].get(name)
+ return partial(f, xpath, name)
+
+def extract_text(xpath, stanza):
+ matched = match(stanza, xpath)
+ return matched[0].text
+
+def save_value(name, func):
+ def f(name, func, stanza, xmpp):
+ xmpp.saved_values[name] = func(stanza)
+ return partial(f, name, func)
+
+def expect_stanza(*args, optional=False, after=None):
+ def f(*xpaths, xmpp, biboumi, optional, after):
+ replacements = common_replacements
+ replacements.update(xmpp.saved_values)
+ check_func = check_xpath if not optional else check_xpath_optional
+ formatted_xpaths = [xpath.format_map(replacements) for xpath in xpaths]
+ xmpp.stanza_checker = partial(check_func, formatted_xpaths, xmpp, after)
+ xmpp.timeout_handler = asyncio.get_event_loop().call_later(10, partial(xmpp.on_timeout, formatted_xpaths))
+ return partial(f, *args, optional=optional, after=after)
+
+def send_stanza(stanza):
+ def internal(stanza, xmpp, biboumi):
+ replacements = common_replacements
+ replacements.update(xmpp.saved_values)
+ xmpp.send_raw(stanza.format_map(replacements))
+ asyncio.get_event_loop().call_soon(xmpp.run_scenario)
+ return partial(internal, stanza)
+
+def expect_unordered(*args):
+ def f(*lists_of_xpaths, xmpp, biboumi):
+ formatted_list_of_xpaths = []
+ for list_of_xpaths in lists_of_xpaths:
+ formatted_xpaths = []
+ for xpath in list_of_xpaths:
+ formatted_xpath = xpath.format_map(common_replacements)
+ formatted_xpaths.append(formatted_xpath)
+ formatted_list_of_xpaths.append(tuple(formatted_xpaths))
+ expect_unordered_already_formatted(formatted_list_of_xpaths, xmpp, biboumi)
+ xmpp.timeout_handler = asyncio.get_event_loop().call_later(10, partial(xmpp.on_timeout, formatted_list_of_xpaths))
+ return partial(f, *args)
+
+def expect_unordered_already_formatted(formatted_list_of_xpaths, xmpp, biboumi):
+ xmpp.stanza_checker = partial(check_list_of_xpath, formatted_list_of_xpaths, xmpp)
+
+def sleep_for(duration):
+ def f(duration, xmpp, biboumi):
+ time.sleep(duration)
+ asyncio.get_event_loop().call_soon(xmpp.run_scenario)
+ return partial(f, duration)
+
+def save_current_timestamp_plus_delta(key, delta):
+ def f(key, delta, message, xmpp):
+ now_plus_delta = datetime.datetime.utcnow() + delta
+ xmpp.saved_values[key] = now_plus_delta.strftime("%FT%T.967Z")
+ return partial(f, key, delta)
diff --git a/tests/end_to_end/scenarios/__init__.py b/tests/end_to_end/scenarios/__init__.py
new file mode 100644
index 0000000..1fef72e
--- /dev/null
+++ b/tests/end_to_end/scenarios/__init__.py
@@ -0,0 +1,10 @@
+# Do "from scenarios import *" instead of repeating these imports everytime in every scenario
+
+from functions import expect_stanza, send_stanza, expect_unordered, save_value, extract_attribute, extract_text, sleep_for, save_current_timestamp_plus_delta
+import datetime
+import sequences
+import scenarios.simple_channel_join
+import scenarios.channel_join_with_two_users
+import scenarios.simple_channel_join_fixed
+import scenarios.channel_join_on_fixed_irc_server
+import scenarios.multiple_channels_join
diff --git a/tests/end_to_end/scenarios/basic_handshake_success.py b/tests/end_to_end/scenarios/basic_handshake_success.py
new file mode 100644
index 0000000..9e1ffb3
--- /dev/null
+++ b/tests/end_to_end/scenarios/basic_handshake_success.py
@@ -0,0 +1,8 @@
+from scenarios import *
+
+# At the start of every scenario, we automatically insert a
+# sequences.handshake() call. So, this scenario is just here to test that
+# this basic thing works fine.
+
+scenario = (
+)
diff --git a/tests/end_to_end/scenarios/basic_subscribe_unsubscribe.py b/tests/end_to_end/scenarios/basic_subscribe_unsubscribe.py
new file mode 100644
index 0000000..6082fa6
--- /dev/null
+++ b/tests/end_to_end/scenarios/basic_subscribe_unsubscribe.py
@@ -0,0 +1,23 @@
+from scenarios import *
+
+scenario = (
+
+
+ # Mutual subscription exchange
+ send_stanza("<presence from='{jid_one}' to='{biboumi_host}' type='subscribe' id='subid1' />"),
+ expect_stanza("/presence[@type='subscribed'][@id='subid1']"),
+
+ # Get the current presence of the biboumi gateway
+ expect_stanza("/presence"),
+
+ expect_stanza("/presence[@type='subscribe']"),
+ send_stanza("<presence from='{jid_one}' to='{biboumi_host}' type='subscribed' />"),
+
+ # Unsubscribe
+ send_stanza("<presence from='{jid_one}' to='{biboumi_host}' type='unsubscribe' id='unsubid1' />"),
+ expect_stanza("/presence[@type='unavailable']"),
+ expect_stanza("/presence[@type='unsubscribed']"),
+ expect_stanza("/presence[@type='unsubscribe']"),
+ send_stanza("<presence from='{jid_one}' to='{biboumi_host}' type='unavailable' />"),
+ send_stanza("<presence from='{jid_one}' to='{biboumi_host}' type='unsubscribed' />"),
+)
diff --git a/tests/end_to_end/scenarios/channel_custom_topic.py b/tests/end_to_end/scenarios/channel_custom_topic.py
new file mode 100644
index 0000000..1fbfb5c
--- /dev/null
+++ b/tests/end_to_end/scenarios/channel_custom_topic.py
@@ -0,0 +1,30 @@
+from scenarios import *
+
+import scenarios.simple_channel_join
+
+scenario = (
+ scenarios.simple_channel_join.scenario,
+
+ # First user sets the topic
+ send_stanza("<message from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}' type='groupchat'><subject>TOPIC TEST</subject></message>"),
+ expect_stanza("/message[@from='#foo%{irc_server_one}/{nick_one}'][@type='groupchat'][@to='{jid_one}/{resource_one}']/subject[text()='TOPIC TEST']"),
+
+ # Second user joins
+ send_stanza("<presence from='{jid_two}/{resource_one}' to='#foo%{irc_server_one}/{nick_two}' />"),
+ sequences.connection("irc.localhost", '{jid_two}/{resource_one}'),
+ expect_unordered(
+ [
+ "/presence[@to='{jid_one}/{resource_one}'][@from='#foo%{irc_server_one}/{nick_two}']/muc_user:x/muc_user:item[@affiliation='none'][@jid='{lower_nick_two}%{irc_server_one}/~{nick_two}@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='{lower_nick_two}%{irc_server_one}/~{nick_two}@localhost'][@role='participant']",
+ "/presence/muc_user:x/muc_user:status[@code='110']"
+ ],
+ [
+ "/message[@from='#foo%{irc_server_one}/{nick_one}'][@type='groupchat']/subject[text()='TOPIC TEST']"
+ ]
+ )
+)
diff --git a/tests/end_to_end/scenarios/channel_force_join.py b/tests/end_to_end/scenarios/channel_force_join.py
new file mode 100644
index 0000000..089da51
--- /dev/null
+++ b/tests/end_to_end/scenarios/channel_force_join.py
@@ -0,0 +1,45 @@
+from scenarios import *
+
+import scenarios.channel_join_with_two_users
+
+scenario = (
+ scenarios.channel_join_with_two_users.scenario,
+ # 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.
+ send_stanza("<presence from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}/{nick_three}'><x xmlns='http://jabber.org/protocol/muc'/></presence>"),
+ 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='{lower_nick_two}%{irc_server_one}/~{nick_two}@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, so everyone receives a nick change
+ 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']",
+ ],
+ ),
+)
+
diff --git a/tests/end_to_end/scenarios/channel_history.py b/tests/end_to_end/scenarios/channel_history.py
new file mode 100644
index 0000000..ade978b
--- /dev/null
+++ b/tests/end_to_end/scenarios/channel_history.py
@@ -0,0 +1,18 @@
+from scenarios import *
+
+scenario = (
+ scenarios.simple_channel_join.scenario,
+
+ # Send one channel message
+ send_stanza("<message from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}' type='groupchat'><body>coucou</body></message>"),
+ expect_stanza("/message[@from='#foo%{irc_server_one}/{nick_one}'][@to='{jid_one}/{resource_one}'][@type='groupchat']/body[text()='coucou']"),
+
+ # Second user joins
+ send_stanza("<presence from='{jid_one}/{resource_two}' to='#foo%{irc_server_one}/{nick_one}' />"),
+ expect_stanza("/presence[@to='{jid_one}/{resource_two}'][@from='#foo%{irc_server_one}/{nick_one}']/muc_user:x/muc_user:item[@affiliation='admin'][@jid='{lower_nick_one}%{irc_server_one}/~{nick_one}@localhost'][@role='moderator']",
+ "/presence/muc_user:x/muc_user:status[@code='110']"),
+ # Receive the history message
+ expect_stanza("/message[@from='#foo%{irc_server_one}/{nick_one}']/body[text()='coucou']",
+ "/message/delay:delay[@from='#foo%{irc_server_one}']"),
+ expect_stanza("/message[@from='#foo%{irc_server_one}'][@type='groupchat']/subject[not(text())]"),
+)
diff --git a/tests/end_to_end/scenarios/channel_history_on_fixed_server.py b/tests/end_to_end/scenarios/channel_history_on_fixed_server.py
new file mode 100644
index 0000000..40a665b
--- /dev/null
+++ b/tests/end_to_end/scenarios/channel_history_on_fixed_server.py
@@ -0,0 +1,20 @@
+from scenarios import *
+
+conf = 'fixed_server'
+
+scenario = (
+ scenarios.channel_join_on_fixed_irc_server.scenario,
+
+ # Send one channel message
+ send_stanza("<message from='{jid_one}/{resource_one}' to='#foo@{biboumi_host}' type='groupchat'><body>coucou</body></message>"),
+ expect_stanza("/message[@from='#foo@{biboumi_host}/{nick_one}'][@to='{jid_one}/{resource_one}'][@type='groupchat']/body[text()='coucou']"),
+
+ # Second user joins
+ send_stanza("<presence from='{jid_one}/{resource_two}' to='#foo@{biboumi_host}/{nick_one}' />"),
+ expect_stanza("/presence[@to='{jid_one}/{resource_two}'][@from='#foo@{biboumi_host}/{nick_one}']/muc_user:x/muc_user:item[@affiliation='admin'][@jid='{lower_nick_one}@{biboumi_host}/~{nick_one}@localhost'][@role='moderator']",
+ "/presence/muc_user:x/muc_user:status[@code='110']"),
+ # Receive the history message
+ expect_stanza("/message[@from='#foo@{biboumi_host}/{nick_one}']/body[text()='coucou']",
+ "/message/delay:delay[@from='#foo@{biboumi_host}']"),
+ expect_stanza("/message[@from='#foo@{biboumi_host}'][@type='groupchat']/subject[not(text())]"),
+)
diff --git a/tests/end_to_end/scenarios/channel_join_on_fixed_irc_server.py b/tests/end_to_end/scenarios/channel_join_on_fixed_irc_server.py
new file mode 100644
index 0000000..1fe9908
--- /dev/null
+++ b/tests/end_to_end/scenarios/channel_join_on_fixed_irc_server.py
@@ -0,0 +1,13 @@
+from scenarios import *
+
+conf = "fixed_server"
+
+scenario = (
+
+ send_stanza("<presence from='{jid_one}/{resource_one}' to='#foo@{biboumi_host}/{nick_one}' />"),
+ sequences.connection("irc.localhost", '{jid_one}/{resource_one}', fixed_irc_server=True),
+ expect_stanza("/message/body[text()='Mode #foo [+nt] by {irc_host_one}']"),
+ expect_stanza("/presence[@to='{jid_one}/{resource_one}'][@from='#foo@{biboumi_host}/{nick_one}']/muc_user:x/muc_user:item[@affiliation='admin'][@role='moderator']",
+ "/presence/muc_user:x/muc_user:status[@code='110']"),
+ expect_stanza("/message[@from='#foo@{biboumi_host}'][@type='groupchat']/subject[not(text())]"),
+)
diff --git a/tests/end_to_end/scenarios/channel_join_with_different_nick.py b/tests/end_to_end/scenarios/channel_join_with_different_nick.py
new file mode 100644
index 0000000..388b098
--- /dev/null
+++ b/tests/end_to_end/scenarios/channel_join_with_different_nick.py
@@ -0,0 +1,14 @@
+from scenarios import *
+
+from scenarios.simple_channel_join import expect_self_join_presence
+
+scenario = (
+ send_stanza("<presence from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}/{nick_one}' />"),
+ sequences.connection("irc.localhost", '{jid_one}/{resource_one}'),
+ expect_self_join_presence(jid = '{jid_one}/{resource_one}', chan = "#foo", nick = "{nick_one}"),
+
+ # The same resource joins a different channel with a different nick
+ send_stanza("<presence from='{jid_one}/{resource_one}' to='#bar%{irc_server_one}/{nick_two}' />"),
+ # We must receive a join presence in response, without any nick change (nick_two) must be ignored
+ expect_self_join_presence(jid = '{jid_one}/{resource_one}', chan = "#bar", nick = "{nick_one}"),
+)
diff --git a/tests/end_to_end/scenarios/channel_join_with_password.py b/tests/end_to_end/scenarios/channel_join_with_password.py
new file mode 100644
index 0000000..4c5e508
--- /dev/null
+++ b/tests/end_to_end/scenarios/channel_join_with_password.py
@@ -0,0 +1,35 @@
+from scenarios import *
+
+import scenarios.simple_channel_join
+
+scenario = (
+ scenarios.simple_channel_join.scenario,
+
+ # Set a password in the room, by using /mode +k
+ send_stanza("<message from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}' type='groupchat'><body>/mode +k SECRET</body></message>"),
+ expect_stanza("/message[@from='#foo%{irc_server_one}'][@to='{jid_one}/{resource_one}'][@type='groupchat']/body[text()='Mode #foo [+k SECRET] by {nick_one}']"),
+
+ # Second user tries to join, without a password (error ensues)
+ send_stanza("<presence from='{jid_two}/{resource_one}' to='#foo%{irc_server_one}/{nick_two}'/>"),
+ sequences.connection("irc.localhost", '{jid_two}/{resource_one}'),
+ expect_stanza("/message/body[text()='{irc_host_one}: #foo: Cannot join channel (+k) - bad key']"),
+ expect_stanza("/presence[@type='error'][@from='#foo%{irc_server_one}/{nick_two}']/error[@type='auth']/stanza:not-authorized"),
+
+ # Second user joins, with the correct password (success)
+ send_stanza("<presence from='{jid_two}/{resource_one}' to='#foo%{irc_server_one}/{nick_two}'> <x xmlns='http://jabber.org/protocol/muc'><password>SECRET</password></x></presence>"),
+ 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='{lower_nick_two}%{irc_server_one}/~{nick_two}@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='{lower_nick_two}%{irc_server_one}/~{nick_two}@localhost'][@role='participant']",
+ "/presence/muc_user:x/muc_user:status[@code='110']"
+ ],
+ [
+ "/message[@from='#foo%{irc_server_one}'][@type='groupchat']/subject[not(text())]"
+ ]
+ )
+)
diff --git a/tests/end_to_end/scenarios/channel_join_with_two_users.py b/tests/end_to_end/scenarios/channel_join_with_two_users.py
new file mode 100644
index 0000000..4e22c50
--- /dev/null
+++ b/tests/end_to_end/scenarios/channel_join_with_two_users.py
@@ -0,0 +1,25 @@
+from scenarios import *
+
+scenario = (
+ scenarios.simple_channel_join.scenario,
+
+ # Second user joins
+ send_stanza("<presence from='{jid_two}/{resource_one}' to='#foo%{irc_server_one}/{nick_two}' />"),
+ sequences.connection("irc.localhost", '{jid_two}/{resource_one}'),
+ 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='{lower_nick_two}%{irc_server_one}/~{nick_two}@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='{lower_nick_two}%{irc_server_one}/~{nick_two}@localhost'][@role='participant']",
+ "/presence/muc_user:x/muc_user:status[@code='110']"
+ ],
+ [
+ "/message[@from='#foo%{irc_server_one}'][@type='groupchat']/subject[not(text())]"
+ ]
+ )
+)
+
diff --git a/tests/end_to_end/scenarios/channel_list_escaping.py b/tests/end_to_end/scenarios/channel_list_escaping.py
new file mode 100644
index 0000000..12c3ff9
--- /dev/null
+++ b/tests/end_to_end/scenarios/channel_list_escaping.py
@@ -0,0 +1,10 @@
+from scenarios import *
+
+scenario = (
+ send_stanza("<presence from='{jid_one}/{resource_one}' to='#true\\2ffalse%{irc_server_one}/{nick_one}' />"),
+ sequences.connection("irc.localhost", '{jid_one}/{resource_one}'),
+ expect_stanza("/message/body[text()='Mode #true/false [+nt] by {irc_host_one}']"),
+ expect_stanza("/presence[@to='{jid_one}/{resource_one}'][@from='#true\\2ffalse%{irc_server_one}/{nick_one}']/muc_user:x/muc_user:item[@affiliation='admin'][@role='moderator']",
+ "/presence/muc_user:x/muc_user:status[@code='110']"),
+ expect_stanza("/message[@from='#true\\2ffalse%{irc_server_one}'][@type='groupchat']/subject[not(text())]"),
+)
diff --git a/tests/end_to_end/scenarios/channel_list_with_rsm.py b/tests/end_to_end/scenarios/channel_list_with_rsm.py
new file mode 100644
index 0000000..79e76f4
--- /dev/null
+++ b/tests/end_to_end/scenarios/channel_list_with_rsm.py
@@ -0,0 +1,67 @@
+from scenarios import *
+
+scenario = (
+ scenarios.simple_channel_join.scenario,
+
+ send_stanza("<presence from='{jid_one}/{resource_one}' to='#bar%{irc_server_one}/{nick_one}' />"),
+ expect_stanza("/message/body[text()='Mode #bar [+nt] by {irc_host_one}']"),
+ expect_stanza("/presence"),
+ expect_stanza("/message[@from='#bar%{irc_server_one}'][@type='groupchat']/subject[not(text())]"),
+
+ send_stanza("<presence from='{jid_one}/{resource_one}' to='#coucou%{irc_server_one}/{nick_one}' />"),
+ expect_stanza("/message/body[text()='Mode #coucou [+nt] by {irc_host_one}']"),
+ expect_stanza("/presence"),
+ expect_stanza("/message[@from='#coucou%{irc_server_one}'][@type='groupchat']/subject[not(text())]"),
+
+ # Ask for 0 item
+ send_stanza("<iq from='{jid_one}/{resource_one}' id='id1' to='{irc_server_one}' type='get'><query xmlns='http://jabber.org/protocol/disco#items'><set xmlns='http://jabber.org/protocol/rsm'><max>0</max></set></query></iq>"),
+
+ # Get 0 item
+ expect_stanza("/iq[@type='result']/disco_items:query"),
+
+ # Ask for 2 (of 3) items We don’t have the count,
+ # because biboumi doesn’t have the complete list when
+ # it sends us the 2 items
+ send_stanza("<iq from='{jid_one}/{resource_one}' id='id1' to='{irc_server_one}' type='get'><query xmlns='http://jabber.org/protocol/disco#items'><set xmlns='http://jabber.org/protocol/rsm'><max>2</max></set></query></iq>"),
+ expect_stanza("/iq[@type='result']/disco_items:query",
+ "/iq/disco_items:query/disco_items:item[@jid='#bar%{irc_server_one}']",
+ "/iq/disco_items:query/disco_items:item[@jid='#coucou%{irc_server_one}']",
+ "/iq/disco_items:query/rsm:set/rsm:first[text()='#bar%{irc_server_one}'][@index='0']",
+ "/iq/disco_items:query/rsm:set/rsm:last[text()='#coucou%{irc_server_one}']"),
+
+ # Ask for 12 (of 3) items. We get the whole list, and thus we have the count included.
+ send_stanza("<iq from='{jid_one}/{resource_one}' id='id1' to='{irc_server_one}' type='get'><query xmlns='http://jabber.org/protocol/disco#items'><set xmlns='http://jabber.org/protocol/rsm'><max>12</max></set></query></iq>"),
+ expect_stanza("/iq[@type='result']/disco_items:query",
+ "/iq/disco_items:query/disco_items:item[@jid='#bar%{irc_server_one}']",
+ "/iq/disco_items:query/disco_items:item[@jid='#coucou%{irc_server_one}']",
+ "/iq/disco_items:query/disco_items:item[@jid='#foo%{irc_server_one}']",
+ "/iq/disco_items:query/rsm:set/rsm:first[text()='#bar%{irc_server_one}'][@index='0']",
+ "/iq/disco_items:query/rsm:set/rsm:last[text()='#foo%{irc_server_one}']",
+ "/iq/disco_items:query/rsm:set/rsm:count[text()='3']"),
+
+ # Ask for 1 item, AFTER the first item (so,
+ # the second). Since we don’t invalidate the cache
+ # with this request, we should have the count
+ # included.
+ send_stanza("<iq from='{jid_one}/{resource_one}' id='id1' to='{irc_server_one}' type='get'><query xmlns='http://jabber.org/protocol/disco#items'><set xmlns='http://jabber.org/protocol/rsm'><after>#bar%{irc_server_one}</after><max>1</max></set></query></iq>"),
+ expect_stanza("/iq[@type='result']/disco_items:query",
+ "/iq/disco_items:query/disco_items:item[@jid='#coucou%{irc_server_one}']",
+ "/iq/disco_items:query/rsm:set/rsm:first[text()='#coucou%{irc_server_one}'][@index='1']",
+ "/iq/disco_items:query/rsm:set/rsm:last[text()='#coucou%{irc_server_one}']",
+ "/iq/disco_items:query/rsm:set/rsm:count[text()='3']"),
+
+ # Ask for 1 item, AFTER the second item (so,
+ # the third).
+ send_stanza("<iq from='{jid_one}/{resource_one}' id='id1' to='{irc_server_one}' type='get'><query xmlns='http://jabber.org/protocol/disco#items'><set xmlns='http://jabber.org/protocol/rsm'><after>#coucou%{irc_server_one}</after><max>1</max></set></query></iq>"),
+ expect_stanza("/iq[@type='result']/disco_items:query",
+ "/iq/disco_items:query/disco_items:item[@jid='#foo%{irc_server_one}']",
+ "/iq/disco_items:query/rsm:set/rsm:first[text()='#foo%{irc_server_one}'][@index='2']",
+ "/iq/disco_items:query/rsm:set/rsm:last[text()='#foo%{irc_server_one}']",
+ "/iq/disco_items:query/rsm:set/rsm:count[text()='3']"),
+
+ # Ask for 1 item, AFTER the third item (so,
+ # the fourth). Since it doesn't exist, we get 0 item
+ send_stanza("<iq from='{jid_one}/{resource_one}' id='id1' to='{irc_server_one}' type='get'><query xmlns='http://jabber.org/protocol/disco#items'><set xmlns='http://jabber.org/protocol/rsm'><after>#foo%{irc_server_one}</after><max>1</max></set></query></iq>"),
+ expect_stanza("/iq[@type='result']/disco_items:query",
+ "/iq/disco_items:query/rsm:set/rsm:count[text()='3']"),
+)
diff --git a/tests/end_to_end/scenarios/channel_messages.py b/tests/end_to_end/scenarios/channel_messages.py
new file mode 100644
index 0000000..8ea979c
--- /dev/null
+++ b/tests/end_to_end/scenarios/channel_messages.py
@@ -0,0 +1,70 @@
+from scenarios import *
+
+import scenarios.simple_channel_join
+
+scenario = (
+ scenarios.simple_channel_join.scenario,
+
+ # Second user joins
+ send_stanza("<presence from='{jid_two}/{resource_one}' to='#foo%{irc_server_one}/{nick_two}' />"),
+ sequences.connection("irc.localhost", '{jid_two}/{resource_one}'),
+
+ # Our presence, sent to the other user, and ourself
+ expect_unordered(
+ ["/presence[@to='{jid_one}/{resource_one}'][@from='#foo%{irc_server_one}/{nick_two}']/muc_user:x/muc_user:item[@affiliation='none'][@jid='{lower_nick_two}%{irc_server_one}/~{nick_two}@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='{lower_nick_two}%{irc_server_one}/~{nick_two}@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 channel message
+ 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
+ expect_unordered(
+ [
+ "/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
+ send_stanza("<message from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}/{nick_two}' type='chat'><body>coucou in private</body></message>"),
+ # Message is received with a server-wide JID
+ expect_stanza("/message[@from='{lower_nick_one}%{irc_server_one}'][@to='{jid_two}/{resource_one}'][@type='chat']/body[text()='coucou in private']"),
+ # Respond to the message, to the server-wide JID
+ send_stanza("<message from='{jid_two}/{resource_one}' to='{lower_nick_one}%{irc_server_one}' type='chat'><body>yes</body></message>"),
+ # The response is received from the in-room JID
+ expect_stanza("/message[@from='#foo%{irc_server_one}/{nick_two}'][@to='{jid_one}/{resource_one}'][@type='chat']/body[text()='yes']",
+ "/message/muc_user:x"),
+ # Do the exact same thing, from a different chan,
+ # to check if the response comes from the right JID
+ send_stanza("<presence from='{jid_one}/{resource_one}' to='#dummy%{irc_server_one}/{nick_one}' />"),
+ expect_stanza("/message"),
+ expect_stanza("/presence/muc_user:x/muc_user:status[@code='110']"),
+ expect_stanza("/message[@from='#dummy%{irc_server_one}'][@type='groupchat']/subject"),
+ # Send a private message, to a in-room JID
+ 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
+ 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
+ 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
+ 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
+ send_stanza("<presence from='{jid_one}/{resource_one}' to='#dummy%{irc_server_one}/{nick_one}' type='unavailable' />"),
+ expect_stanza("/presence[@type='unavailable']/muc_user:x/muc_user:status[@code='110']"),
+
+ # The private messages from this nick should now come (again) from the server-wide JID
+ send_stanza("<message from='{jid_two}/{resource_one}' to='{lower_nick_one}%{irc_server_one}' type='chat'><body>hihihoho</body></message>"),
+ expect_stanza("/message[@from='{lower_nick_two}%{irc_server_one}'][@to='{jid_one}/{resource_one}']"),
+)
diff --git a/tests/end_to_end/scenarios/client_error.py b/tests/end_to_end/scenarios/client_error.py
new file mode 100644
index 0000000..ca5eaee
--- /dev/null
+++ b/tests/end_to_end/scenarios/client_error.py
@@ -0,0 +1,16 @@
+from scenarios import *
+
+scenario = (
+ scenarios.simple_channel_join.scenario,
+ # Second resource, same channel
+ send_stanza("<presence from='{jid_one}/{resource_two}' to='#foo%{irc_server_one}/{nick_one}' />"),
+ expect_stanza("/presence[@to='{jid_one}/{resource_two}'][@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']"),
+ expect_stanza("/message[@from='#foo%{irc_server_one}'][@type='groupchat'][@to='{jid_one}/{resource_two}']/subject[not(text())]"),
+
+ # Now the first resource has an error
+ send_stanza("<message from='{jid_one}/{resource_one}' to='#foo%%{irc_server_one}/{nick_one}' type='error'><error type='cancel'><recipient-unavailable xmlns='urn:ietf:params:xml:ns:xmpp-stanzas'/></error></message>"),
+ # Receive a leave only to the leaving resource
+ expect_stanza("/presence[@type='unavailable'][@from='#foo%{irc_server_one}/{nick_one}'][@to='{jid_one}/{resource_one}']/muc_user:x/muc_user:status[@code='110']",
+ "/presence/status[text()='Biboumi note: 1 resources are still in this channel.']"),
+)
diff --git a/tests/end_to_end/scenarios/complete_channel_list_with_pages_of_3.py b/tests/end_to_end/scenarios/complete_channel_list_with_pages_of_3.py
new file mode 100644
index 0000000..8cef926
--- /dev/null
+++ b/tests/end_to_end/scenarios/complete_channel_list_with_pages_of_3.py
@@ -0,0 +1,106 @@
+from scenarios import *
+
+scenario = (
+ send_stanza("<presence from='{jid_one}/{resource_one}' to='#aaa%{irc_server_one}/{nick_one}' />"),
+ sequences.connection("irc.localhost", '{jid_one}/{resource_one}'),
+ expect_stanza("/message"),
+ expect_stanza("/presence"),
+ expect_stanza("/message"),
+
+ send_stanza("<presence from='{jid_one}/{resource_one}' to='#bbb%{irc_server_one}/{nick_one}' />"),
+ expect_stanza("/message"),
+ expect_stanza("/presence"),
+ expect_stanza("/message"),
+
+ send_stanza("<presence from='{jid_one}/{resource_one}' to='#ccc%{irc_server_one}/{nick_one}' />"),
+ expect_stanza("/message"),
+ expect_stanza("/presence"),
+ expect_stanza("/message"),
+
+ send_stanza("<presence from='{jid_one}/{resource_one}' to='#ddd%{irc_server_one}/{nick_one}' />"),
+ expect_stanza("/message"),
+ expect_stanza("/presence"),
+ expect_stanza("/message"),
+
+ send_stanza("<presence from='{jid_one}/{resource_one}' to='#eee%{irc_server_one}/{nick_one}' />"),
+ expect_stanza("/message"),
+ expect_stanza("/presence"),
+ expect_stanza("/message"),
+
+ send_stanza("<presence from='{jid_one}/{resource_one}' to='#fff%{irc_server_one}/{nick_one}' />"),
+ expect_stanza("/message"),
+ expect_stanza("/presence"),
+ expect_stanza("/message"),
+
+ send_stanza("<presence from='{jid_one}/{resource_one}' to='#ggg%{irc_server_one}/{nick_one}' />"),
+ expect_stanza("/message"),
+ expect_stanza("/presence"),
+ expect_stanza("/message"),
+
+ send_stanza("<presence from='{jid_one}/{resource_one}' to='#hhh%{irc_server_one}/{nick_one}' />"),
+ expect_stanza("/message"),
+ expect_stanza("/presence"),
+ expect_stanza("/message"),
+
+ send_stanza("<presence from='{jid_one}/{resource_one}' to='#iii%{irc_server_one}/{nick_one}' />"),
+ expect_stanza("/message"),
+ expect_stanza("/presence"),
+ expect_stanza("/message"),
+
+ send_stanza("<presence from='{jid_one}/{resource_one}' to='#jjj%{irc_server_one}/{nick_one}' />"),
+ expect_stanza("/message"),
+ expect_stanza("/presence"),
+ expect_stanza("/message"),
+
+ send_stanza("<iq from='{jid_one}/{resource_one}' id='id' to='{irc_server_one}' type='get'><query xmlns='http://jabber.org/protocol/disco#items'><set xmlns='http://jabber.org/protocol/rsm'><max>3</max></set></query></iq>"),
+ expect_stanza("/iq[@type='result']/disco_items:query",
+ "/iq/disco_items:query/disco_items:item[@jid='#aaa%{irc_server_one}']",
+ "/iq/disco_items:query/disco_items:item[@jid='#bbb%{irc_server_one}']",
+ "/iq/disco_items:query/disco_items:item[@jid='#ccc%{irc_server_one}']",
+ "/iq/disco_items:query/rsm:set/rsm:first[text()='#aaa%{irc_server_one}'][@index='0']",
+ "/iq/disco_items:query/rsm:set/rsm:last[text()='#ccc%{irc_server_one}']"),
+
+ send_stanza("<iq from='{jid_one}/{resource_one}' id='id' to='{irc_server_one}' type='get'><query xmlns='http://jabber.org/protocol/disco#items'><set xmlns='http://jabber.org/protocol/rsm'><after>#ccc%{irc_server_one}</after><max>3</max></set></query></iq>"),
+ expect_stanza("/iq[@type='result']/disco_items:query",
+ "/iq/disco_items:query/disco_items:item[@jid='#ddd%{irc_server_one}']",
+ "/iq/disco_items:query/disco_items:item[@jid='#eee%{irc_server_one}']",
+ "/iq/disco_items:query/disco_items:item[@jid='#fff%{irc_server_one}']",
+ "/iq/disco_items:query/rsm:set/rsm:first[text()='#ddd%{irc_server_one}'][@index='3']",
+ "/iq/disco_items:query/rsm:set/rsm:last[text()='#fff%{irc_server_one}']"),
+
+ send_stanza("<iq from='{jid_one}/{resource_one}' id='id' to='{irc_server_one}' type='get'><query xmlns='http://jabber.org/protocol/disco#items'><set xmlns='http://jabber.org/protocol/rsm'><after>#fff%{irc_server_one}</after><max>3</max></set></query></iq>"),
+ expect_stanza("/iq[@type='result']/disco_items:query",
+ "/iq/disco_items:query/disco_items:item[@jid='#ggg%{irc_server_one}']",
+ "/iq/disco_items:query/disco_items:item[@jid='#hhh%{irc_server_one}']",
+ "/iq/disco_items:query/disco_items:item[@jid='#iii%{irc_server_one}']",
+ "/iq/disco_items:query/rsm:set/rsm:first[text()='#ggg%{irc_server_one}'][@index='6']",
+ "/iq/disco_items:query/rsm:set/rsm:last[text()='#iii%{irc_server_one}']"),
+
+ send_stanza("<iq from='{jid_one}/{resource_one}' id='id' to='{irc_server_one}' type='get'><query xmlns='http://jabber.org/protocol/disco#items'><set xmlns='http://jabber.org/protocol/rsm'><after>#iii%{irc_server_one}</after><max>3</max></set></query></iq>"),
+ expect_stanza("/iq[@type='result']/disco_items:query",
+ "/iq/disco_items:query/disco_items:item[@jid='#jjj%{irc_server_one}']",
+ "/iq/disco_items:query/rsm:set/rsm:first[text()='#jjj%{irc_server_one}'][@index='9']",
+ "/iq/disco_items:query/rsm:set/rsm:last[text()='#jjj%{irc_server_one}']",
+ "/iq/disco_items:query/rsm:set/rsm:count[text()='10']"),
+
+ send_stanza("<presence from='{jid_one}/{resource_one}' to='#aaa%{irc_server_one}/{nick_one}' type='unavailable' />"),
+ send_stanza("<presence from='{jid_one}/{resource_one}' to='#bbb%{irc_server_one}/{nick_one}' type='unavailable' />"),
+ send_stanza("<presence from='{jid_one}/{resource_one}' to='#ccc%{irc_server_one}/{nick_one}' type='unavailable' />"),
+ send_stanza("<presence from='{jid_one}/{resource_one}' to='#ddd%{irc_server_one}/{nick_one}' type='unavailable' />"),
+ send_stanza("<presence from='{jid_one}/{resource_one}' to='#eee%{irc_server_one}/{nick_one}' type='unavailable' />"),
+ send_stanza("<presence from='{jid_one}/{resource_one}' to='#fff%{irc_server_one}/{nick_one}' type='unavailable' />"),
+ send_stanza("<presence from='{jid_one}/{resource_one}' to='#ggg%{irc_server_one}/{nick_one}' type='unavailable' />"),
+ send_stanza("<presence from='{jid_one}/{resource_one}' to='#hhh%{irc_server_one}/{nick_one}' type='unavailable' />"),
+ send_stanza("<presence from='{jid_one}/{resource_one}' to='#iii%{irc_server_one}/{nick_one}' type='unavailable' />"),
+ send_stanza("<presence from='{jid_one}/{resource_one}' to='#jjj%{irc_server_one}/{nick_one}' type='unavailable' />"),
+ expect_stanza("/presence[@type='unavailable']"),
+ expect_stanza("/presence[@type='unavailable']"),
+ expect_stanza("/presence[@type='unavailable']"),
+ expect_stanza("/presence[@type='unavailable']"),
+ expect_stanza("/presence[@type='unavailable']"),
+ expect_stanza("/presence[@type='unavailable']"),
+ expect_stanza("/presence[@type='unavailable']"),
+ expect_stanza("/presence[@type='unavailable']"),
+ expect_stanza("/presence[@type='unavailable']"),
+ expect_stanza("/presence[@type='unavailable']"),
+)
diff --git a/tests/end_to_end/scenarios/configure_bad_value.py b/tests/end_to_end/scenarios/configure_bad_value.py
new file mode 100644
index 0000000..4d2575c
--- /dev/null
+++ b/tests/end_to_end/scenarios/configure_bad_value.py
@@ -0,0 +1,21 @@
+from scenarios import *
+
+scenario = (
+ # Configure the throttle option with an incorrect value
+ 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>"),
+ expect_stanza("/iq[@type='result']",
+ after = save_value("sessionid", extract_attribute("/iq[@type='result']/commands:command[@node='configure']", "sessionid"))),
+ 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='throttle_limit'><value>bleh</value></field>"
+ "<field var='max_history_length'><value>bleh</value></field>"
+ "</x></command></iq>"),
+ expect_stanza("/iq[@type='result']/commands:command[@node='configure'][@status='completed']"),
+
+ # These options should have their default value
+ 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' action='execute' /></iq>"),
+ expect_stanza("/iq[@type='result']",
+ "/iq/commands:command/dataform:x[@type='form']/dataform:field[@type='text-single'][@var='throttle_limit']/dataform:value[text()='10']",
+ "/iq/commands:command/dataform:x[@type='form']/dataform:field[@type='text-single'][@var='max_history_length']/dataform:value[text()='20']"),
+)
diff --git a/tests/end_to_end/scenarios/default_channel_list_limit.py b/tests/end_to_end/scenarios/default_channel_list_limit.py
new file mode 100644
index 0000000..84f96b3
--- /dev/null
+++ b/tests/end_to_end/scenarios/default_channel_list_limit.py
@@ -0,0 +1,52 @@
+from scenarios import *
+
+def incr_counter():
+ counter = -1
+ def f(stanza):
+ nonlocal counter
+ counter += 1
+ return counter
+ return f
+
+counter = incr_counter()
+
+scenario = (
+ # Disable the throttling, otherwise it’s way too long
+ 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>"),
+ expect_stanza("/iq[@type='result']",
+ after = save_value("sessionid", extract_attribute("/iq[@type='result']/commands:command[@node='configure']", "sessionid"))),
+ 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>"),
+ expect_stanza("/iq[@type='result']/commands:command[@node='configure'][@status='completed']/commands:note[@type='info'][text()='Configuration successfully applied.']",
+ after = save_value("counter", counter)),
+
+
+ send_stanza("<presence from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}/{nick_one}' />"),
+ sequences.connection(),
+
+ scenarios.simple_channel_join.expect_self_join_presence(jid = '{jid_one}/{resource_one}', chan = "#foo", nick = "{nick_one}"),
+
+
+ (
+ send_stanza("<presence from='{jid_one}/{resource_one}' to='#{counter}%{irc_server_one}/{nick_one}' />"),
+ expect_stanza("/message"),
+ expect_stanza("/presence",
+ after = save_value("counter", counter)),
+ expect_stanza("/message"),
+ ) * 110,
+
+ send_stanza("<iq from='{jid_one}/{resource_one}' id='id1' to='{irc_server_one}' type='get'><query xmlns='http://jabber.org/protocol/disco#items'/></iq>"),
+ # charybdis sends the list in alphabetic order, so #foo is the last, and #99 is after #120
+ expect_stanza("/iq/disco_items:query/disco_items:item[@jid='#0%{irc_server_one}']",
+ "/iq/disco_items:query/disco_items:item[@jid='#1%{irc_server_one}']",
+ "/iq/disco_items:query/disco_items:item[@jid='#109%{irc_server_one}']",
+ "/iq/disco_items:query/disco_items:item[@jid='#9%{irc_server_one}']",
+ "!/iq/disco_items:query/disco_items:item[@jid='#foo%{irc_server_one}']",
+ "!/iq/disco_items:query/disco_items:item[@jid='#99%{irc_server_one}']",
+ "!/iq/disco_items:query/disco_items:item[@jid='#90%{irc_server_one}']"),
+)
diff --git a/tests/end_to_end/scenarios/default_mam_limit.py b/tests/end_to_end/scenarios/default_mam_limit.py
new file mode 100644
index 0000000..02fcaa7
--- /dev/null
+++ b/tests/end_to_end/scenarios/default_mam_limit.py
@@ -0,0 +1,105 @@
+from scenarios import *
+
+scenario = (
+ # Disable the throttling, otherwise it’s way too long
+ 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>"),
+ expect_stanza("/iq[@type='result']",
+ after = save_value("sessionid", extract_attribute("/iq[@type='result']/commands:command[@node='configure']", "sessionid"))),
+ 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>"),
+ expect_stanza("/iq[@type='result']/commands:command[@node='configure'][@status='completed']/commands:note[@type='info'][text()='Configuration successfully applied.']"),
+
+ send_stanza("<presence from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}/{nick_one}' />"),
+ sequences.connection("irc.localhost", '{jid_one}/{resource_one}'),
+ expect_stanza("/message/body[text()='Mode #foo [+nt] by {irc_host_one}']"),
+ 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']"),
+ expect_stanza("/message[@from='#foo%{irc_server_one}'][@type='groupchat']/subject[not(text())]",
+ after = save_value("counter", lambda x: 0)),
+ (
+ send_stanza("<message from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}' type='groupchat'><body>{counter}</body></message>"),
+ expect_stanza("/message[@from='#foo%{irc_server_one}/{nick_one}'][@to='{jid_one}/{resource_one}'][@type='groupchat']/body[text()='{counter}']",
+ after = save_value("counter", lambda stanza: str(1 + int(extract_text("/message/body", stanza))))),
+ ) * 150,
+
+ # Retrieve the archive, without any restriction
+ 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>"),
+ 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()='0']"),
+ # followed by 98 more messages
+ (
+ 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"),
+ ) * 98,
+
+ # and finally the message "99"
+ 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()='99']",
+ after = save_value("last_uuid", extract_attribute("/message/mam:result", "id"))),
+
+ # And it should not be marked as complete
+ 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
+ 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>"),
+
+ 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']"),
+ (
+ 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"),
+ ) * 48,
+ 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 = save_value("last_uuid", extract_attribute("/message/mam:result", "id"))),
+ 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.
+ 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>"),
+ 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
+ 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>"),
+
+ 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']"),
+ (
+ 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"),
+ ) * 98,
+ 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 = save_value("last_uuid", extract_attribute("/message/mam:result", "id"))),
+ 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.
+ 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>"),
+ 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']"),
+ 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 = save_value("last_uuid", extract_attribute("/message/mam:result", "id"))),
+ 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
+ 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>"),
+
+ expect_stanza("/iq[@type='result'][@id='id5'][@from='#foo%{irc_server_one}'][@to='{jid_one}/{resource_one}']",
+ "!/iq/mam:fin[@complete='true']"),
+)
diff --git a/tests/end_to_end/scenarios/encoded_channel_join.py b/tests/end_to_end/scenarios/encoded_channel_join.py
new file mode 100644
index 0000000..fd4144a
--- /dev/null
+++ b/tests/end_to_end/scenarios/encoded_channel_join.py
@@ -0,0 +1,10 @@
+from scenarios import *
+
+scenario = (
+ send_stanza("<presence from='{jid_one}/{resource_one}' to='#biboumi\\40louiz.org\\3a80%{irc_server_one}/{nick_one}' />"),
+ sequences.connection("irc.localhost", '{jid_one}/{resource_one}'),
+ expect_stanza("/message/body[text()='Mode #biboumi@louiz.org:80 [+nt] by {irc_host_one}']"),
+ expect_stanza("/presence[@to='{jid_one}/{resource_one}'][@from='#biboumi\\40louiz.org\\3a80%{irc_server_one}/{nick_one}']/muc_user:x/muc_user:item[@affiliation='admin'][@role='moderator']",
+ "/presence/muc_user:x/muc_user:status[@code='110']"),
+ expect_stanza("/message[@from='#biboumi\\40louiz.org\\3a80%{irc_server_one}'][@type='groupchat']/subject[not(text())]"),
+)
diff --git a/tests/end_to_end/scenarios/execute_admin_disconnect_from_server_adhoc_command.py b/tests/end_to_end/scenarios/execute_admin_disconnect_from_server_adhoc_command.py
new file mode 100644
index 0000000..18dfe94
--- /dev/null
+++ b/tests/end_to_end/scenarios/execute_admin_disconnect_from_server_adhoc_command.py
@@ -0,0 +1,68 @@
+from scenarios import *
+
+from scenarios.simple_channel_join import expect_self_join_presence
+
+scenario = (
+ # Admin connects to first server
+ send_stanza("<presence from='{jid_admin}/{resource_one}' to='#bar%{irc_server_one}/{nick_one}' />"),
+ sequences.connection("irc.localhost", '{jid_admin}/{resource_one}'),
+ expect_self_join_presence(jid = '{jid_admin}/{resource_one}', chan = "#bar", nick = "{nick_one}"),
+
+ # Non-Admin connects to first server
+ send_stanza("<presence from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}/{nick_two}' />"),
+ sequences.connection("irc.localhost", '{jid_one}/{resource_one}'),
+ expect_self_join_presence(jid = '{jid_one}/{resource_one}', chan = "#foo", nick = "{nick_two}"),
+
+ # Non-admin connects to second server
+ send_stanza("<presence from='{jid_one}/{resource_two}' to='#bon%{irc_server_two}/{nick_three}' />"),
+ sequences.connection("{irc_host_two}", '{jid_one}/{resource_two}'),
+ expect_self_join_presence(jid = '{jid_one}/{resource_two}', chan = "#bon", nick = "{nick_three}", irc_server = "{irc_server_two}"),
+
+ # Execute as admin
+ send_stanza("<iq type='set' id='command1' from='{jid_admin}/{resource_one}' to='{biboumi_host}'><command xmlns='http://jabber.org/protocol/commands' node='disconnect-from-irc-server' action='execute' /></iq>"),
+ expect_stanza("/iq[@type='result']/commands:command[@node='disconnect-from-irc-server'][@sessionid][@status='executing']",
+ "/iq/commands:command/dataform:x[@type='form']/dataform:field[@var='jid'][@type='list-single']/dataform:option[@label='{jid_one}']/dataform:value[text()='{jid_one}']",
+ "/iq/commands:command/dataform:x[@type='form']/dataform:field[@var='jid'][@type='list-single']/dataform:option[@label='{jid_admin}']/dataform:value[text()='{jid_admin}']",
+ "/iq/commands:command/commands:actions/commands:next",
+ after = save_value("sessionid", extract_attribute("/iq/commands:command[@node='disconnect-from-irc-server']", "sessionid"))
+ ),
+ send_stanza("<iq type='set' id='command2' from='{jid_admin}/{resource_one}' to='{biboumi_host}'><command xmlns='http://jabber.org/protocol/commands' node='disconnect-from-irc-server' sessionid='{sessionid}' action='next'><x xmlns='jabber:x:data' type='submit'><field var='jid'><value>{jid_one}</value></field><field var='quit-message'><value>e2e test one</value></field></x></command></iq>"),
+ expect_stanza("/iq[@type='result']/commands:command[@node='disconnect-from-irc-server'][@sessionid][@status='executing']",
+ "/iq/commands:command/dataform:x[@type='form']/dataform:field[@var='quit-message'][@type='text-single']",
+ "/iq/commands:command/dataform:x[@type='form']/dataform:field[@var='irc-servers'][@type='list-multi']",
+ "/iq/commands:command/dataform:x[@type='form']/dataform:field[@var='irc-servers']/dataform:option[@label='localhost']/dataform:value[text()='localhost']",
+ "/iq/commands:command/dataform:x[@type='form']/dataform:field[@var='irc-servers']/dataform:option[@label='irc.localhost']/dataform:value[text()='irc.localhost']",
+ "/iq/commands:command/commands:actions/commands:complete",
+ after = save_value("sessionid", extract_attribute("/iq/commands:command[@node='disconnect-from-irc-server']", "sessionid"))
+ ),
+ # Command is successfull
+ send_stanza("<iq type='set' id='command3' from='{jid_admin}/{resource_one}' to='{biboumi_host}'><command xmlns='http://jabber.org/protocol/commands' node='disconnect-from-irc-server' sessionid='{sessionid}' action='complete'><x xmlns='jabber:x:data' type='submit'><field var='irc-servers'><value>localhost</value></field><field var='quit-message'><value>Disconnected by e2e</value></field></x></command></iq>"),
+ # User is being disconnected
+ expect_unordered(
+ [
+ "/presence[@type='unavailable'][@to='{jid_one}/{resource_two}'][@from='#bon%{irc_server_two}/{nick_three}']",
+ "/presence/status[text()='Disconnected by e2e']"
+ ],
+ [
+ "/iq[@type='result']/commands:command[@node='disconnect-from-irc-server'][@status='completed']/commands:note[@type='info'][text()='{jid_one} was disconnected from 1 IRC server.']",
+ ]),
+
+ # Execute as non-admin (this skips the first step)
+ send_stanza("<iq type='set' id='command4' from='{jid_one}/{resource_one}' to='{biboumi_host}'><command xmlns='http://jabber.org/protocol/commands' node='disconnect-from-irc-server' action='execute' /></iq>"),
+ expect_stanza("/iq[@type='result']/commands:command[@node='disconnect-from-irc-server'][@sessionid][@status='executing']",
+ "/iq/commands:command/dataform:x[@type='form']/dataform:field[@var='quit-message'][@type='text-single']",
+ "/iq/commands:command/dataform:x[@type='form']/dataform:field[@var='irc-servers'][@type='list-multi']",
+ "/iq/commands:command/dataform:x[@type='form']/dataform:field[@var='irc-servers']/dataform:option[@label='irc.localhost']/dataform:value[text()='irc.localhost']",
+ "/iq/commands:command/commands:actions/commands:complete",
+ after = save_value("sessionid", extract_attribute("/iq/commands:command[@node='disconnect-from-irc-server']", "sessionid"))
+ ),
+ send_stanza("<iq type='set' id='command5' from='{jid_one}/{resource_one}' to='{biboumi_host}'><command xmlns='http://jabber.org/protocol/commands' node='disconnect-from-irc-server' sessionid='{sessionid}' action='complete'><x xmlns='jabber:x:data' type='submit'><field var='irc-servers'><value>irc.localhost</value></field><field var='quit-message'><value>Disconnected by e2e</value></field></x></command></iq>"),
+ expect_unordered(
+ [
+ "/presence[@type='unavailable'][@to='{jid_one}/{resource_one}'][@from='#foo%{irc_server_one}/{nick_two}']",
+ "/presence/status[text()='Disconnected by e2e']"
+ ],
+ [
+ "/iq[@type='result']/commands:command[@node='disconnect-from-irc-server'][@status='completed']/commands:note[@type='info'][text()='{jid_one}/{resource_one} was disconnected from 1 IRC server.']",
+ ]),
+)
diff --git a/tests/end_to_end/scenarios/execute_disconnect_user_adhoc_command.py b/tests/end_to_end/scenarios/execute_disconnect_user_adhoc_command.py
new file mode 100644
index 0000000..a680017
--- /dev/null
+++ b/tests/end_to_end/scenarios/execute_disconnect_user_adhoc_command.py
@@ -0,0 +1,19 @@
+from scenarios import *
+
+from scenarios.simple_channel_join import expect_self_join_presence
+
+scenario = (
+ send_stanza("<presence from='{jid_admin}/{resource_one}' to='#foo%{irc_server_one}/{nick_one}' />"),
+ sequences.connection("irc.localhost", '{jid_admin}/{resource_one}'),
+ expect_self_join_presence(jid = '{jid_admin}/{resource_one}', chan = "#foo", nick = "{nick_one}"),
+
+ send_stanza("<iq type='set' id='command1' from='{jid_admin}/{resource_one}' to='{biboumi_host}'><command xmlns='http://jabber.org/protocol/commands' node='disconnect-user' action='execute' /></iq>"),
+ expect_stanza("/iq[@type='result']/commands:command[@node='disconnect-user'][@sessionid][@status='executing']",
+ "/iq/commands:command/commands:actions/commands:complete",
+ after = save_value("sessionid", extract_attribute("/iq/commands:command[@node='disconnect-user']", "sessionid"))
+ ),
+ send_stanza("<iq type='set' id='command2' from='{jid_admin}/{resource_one}' to='{biboumi_host}'><command xmlns='http://jabber.org/protocol/commands' node='disconnect-user' sessionid='{sessionid}' action='complete'><x xmlns='jabber:x:data' type='submit'><field var='jids'><value>{jid_admin}</value></field><field var='quit-message'><value>Disconnected by e2e</value></field></x></command></iq>"),
+ expect_stanza("/iq[@type='result']/commands:command[@node='disconnect-user'][@status='completed']/commands:note[@type='info'][text()='1 user has been disconnected.']"),
+ # Note, charybdis ignores our QUIT message, so we can't test it
+ expect_stanza("/presence[@type='unavailable'][@to='{jid_admin}/{resource_one}'][@from='#foo%{irc_server_one}/{nick_one}']"),
+)
diff --git a/tests/end_to_end/scenarios/execute_forbidden_adhoc_command.py b/tests/end_to_end/scenarios/execute_forbidden_adhoc_command.py
new file mode 100644
index 0000000..10c98ab
--- /dev/null
+++ b/tests/end_to_end/scenarios/execute_forbidden_adhoc_command.py
@@ -0,0 +1,7 @@
+from scenarios import *
+
+scenario = (
+ send_stanza("<iq type='set' id='command1' from='{jid_one}/{resource_one}' to='{biboumi_host}'><command xmlns='http://jabber.org/protocol/commands' node='disconnect-user' action='execute' /></iq>"),
+ expect_stanza("/iq[@type='error'][@id='command1']/commands:command[@node='disconnect-user']",
+ "/iq/commands:command/commands:error[@type='cancel']/stanza:forbidden"),
+)
diff --git a/tests/end_to_end/scenarios/execute_hello_adhoc_command.py b/tests/end_to_end/scenarios/execute_hello_adhoc_command.py
new file mode 100644
index 0000000..916d95a
--- /dev/null
+++ b/tests/end_to_end/scenarios/execute_hello_adhoc_command.py
@@ -0,0 +1,14 @@
+from scenarios import *
+
+scenario = (
+ send_stanza("<iq type='set' id='hello-command1' from='{jid_one}/{resource_one}' to='{biboumi_host}'><command xmlns='http://jabber.org/protocol/commands' node='hello' action='execute' /></iq>"),
+ expect_stanza("/iq[@type='result']/commands:command[@node='hello'][@sessionid][@status='executing']",
+ "/iq/commands:command/dataform:x[@type='form']/dataform:title[text()='Configure your name.']",
+ "/iq/commands:command/dataform:x[@type='form']/dataform:instructions[text()='Please provide your name.']",
+ "/iq/commands:command/dataform:x[@type='form']/dataform:field[@type='text-single']/dataform:required",
+ "/iq/commands:command/commands:actions/commands:complete",
+ after = save_value("sessionid", extract_attribute("/iq[@type='result']/commands:command[@node='hello']", "sessionid"))
+ ),
+ send_stanza("<iq type='set' id='hello-command2' from='{jid_one}/{resource_one}' to='{biboumi_host}'><command xmlns='http://jabber.org/protocol/commands' node='hello' sessionid='{sessionid}' action='complete'><x xmlns='jabber:x:data' type='submit'><field var='name'><value>COUCOU</value></field></x></command></iq>"),
+ expect_stanza("/iq[@type='result']/commands:command[@node='hello'][@status='completed']/commands:note[@type='info'][text()='Hello COUCOU!']")
+)
diff --git a/tests/end_to_end/scenarios/execute_incomplete_hello_adhoc_command.py b/tests/end_to_end/scenarios/execute_incomplete_hello_adhoc_command.py
new file mode 100644
index 0000000..83b2a55
--- /dev/null
+++ b/tests/end_to_end/scenarios/execute_incomplete_hello_adhoc_command.py
@@ -0,0 +1,11 @@
+from scenarios import *
+
+scenario = (
+ send_stanza("<iq type='set' id='hello-command1' from='{jid_one}/{resource_one}' to='{biboumi_host}'><command xmlns='http://jabber.org/protocol/commands' node='hello' action='execute' /></iq>"),
+ expect_stanza("/iq[@type='result']/commands:command[@node='hello'][@sessionid][@status='executing']",
+ "/iq/commands:command/commands:actions/commands:complete",
+ after = save_value("sessionid", extract_attribute("/iq[@type='result']/commands:command[@node='hello']", "sessionid"))
+ ),
+ send_stanza("<iq type='set' id='hello-command2' from='{jid_one}/{resource_one}' to='{biboumi_host}'><command xmlns='http://jabber.org/protocol/commands' node='hello' sessionid='{sessionid}' action='complete'><x xmlns='jabber:x:data' type='submit'></x></command></iq>"),
+ expect_stanza("/iq[@type='error']")
+)
diff --git a/tests/end_to_end/scenarios/execute_ping_adhoc_command.py b/tests/end_to_end/scenarios/execute_ping_adhoc_command.py
new file mode 100644
index 0000000..bcdefe1
--- /dev/null
+++ b/tests/end_to_end/scenarios/execute_ping_adhoc_command.py
@@ -0,0 +1,6 @@
+from scenarios import *
+
+scenario = (
+ send_stanza("<iq type='set' id='ping-command1' from='{jid_one}/{resource_one}' to='{biboumi_host}'><command xmlns='http://jabber.org/protocol/commands' node='ping' action='execute' /></iq>"),
+ expect_stanza("/iq[@type='result']/commands:command[@node='ping'][@status='completed']/commands:note[@type='info'][text()='Pong']")
+)
diff --git a/tests/end_to_end/scenarios/execute_reload_adhoc_command.py b/tests/end_to_end/scenarios/execute_reload_adhoc_command.py
new file mode 100644
index 0000000..5c4e1f7
--- /dev/null
+++ b/tests/end_to_end/scenarios/execute_reload_adhoc_command.py
@@ -0,0 +1,6 @@
+from scenarios import *
+
+scenario = (
+ send_stanza("<iq type='set' id='ping-command1' from='{jid_admin}/{resource_one}' to='{biboumi_host}'><command xmlns='http://jabber.org/protocol/commands' node='reload' action='execute' /></iq>"),
+ expect_stanza("/iq[@type='result']/commands:command[@node='reload'][@status='completed']/commands:note[@type='info'][text()='Configuration reloaded.']"),
+)
diff --git a/tests/end_to_end/scenarios/fixed_irc_server_subscription.py b/tests/end_to_end/scenarios/fixed_irc_server_subscription.py
new file mode 100644
index 0000000..091cf2a
--- /dev/null
+++ b/tests/end_to_end/scenarios/fixed_irc_server_subscription.py
@@ -0,0 +1,8 @@
+from scenarios import *
+
+conf = 'fixed_server'
+
+scenario = (
+ send_stanza("<presence type='subscribe' from='{jid_one}/{resource_one}' to='{biboumi_host}' id='sub1' />"),
+ expect_stanza("/presence[@to='{jid_one}'][@from='{biboumi_host}'][@type='subscribed']")
+)
diff --git a/tests/end_to_end/scenarios/fixed_muc_disco_info.py b/tests/end_to_end/scenarios/fixed_muc_disco_info.py
new file mode 100644
index 0000000..6cabb49
--- /dev/null
+++ b/tests/end_to_end/scenarios/fixed_muc_disco_info.py
@@ -0,0 +1,14 @@
+from scenarios import *
+
+conf = 'fixed_server'
+
+scenario = (
+ send_stanza("<iq from='{jid_one}/{resource_one}' to='#foo@{biboumi_host}' id='1' type='get'><query xmlns='http://jabber.org/protocol/disco#info'/></iq>"),
+ 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='#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']"),
+)
diff --git a/tests/end_to_end/scenarios/get_irc_connection_info.py b/tests/end_to_end/scenarios/get_irc_connection_info.py
new file mode 100644
index 0000000..25f2a87
--- /dev/null
+++ b/tests/end_to_end/scenarios/get_irc_connection_info.py
@@ -0,0 +1,15 @@
+from scenarios import *
+
+scenario = (
+ send_stanza("<iq type='set' id='command1' from='{jid_one}/{resource_one}' to='{irc_server_one}'><command xmlns='http://jabber.org/protocol/commands' node='get-irc-connection-info' action='execute' /></iq>"),
+ expect_stanza("/iq/commands:command/commands:note[text()='You are not connected to the IRC server irc.localhost']"),
+
+ send_stanza("<presence from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}/{nick_one}' />"),
+ sequences.connection("irc.localhost", '{jid_one}/{resource_one}'),
+ expect_stanza("/message"),
+ expect_stanza("/presence"),
+ expect_stanza("/message"),
+
+ send_stanza("<iq type='set' id='command2' from='{jid_one}/{resource_one}' to='{irc_server_one}'><command xmlns='http://jabber.org/protocol/commands' node='get-irc-connection-info' action='execute' /></iq>"),
+ expect_stanza(r"/iq/commands:command/commands:note[re:test(text(), 'Connected to IRC server irc.localhost on port 6667 since \d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d \(\d+ seconds ago\)\.\n#foo from 1 resource: {resource_one}.*')]"),
+)
diff --git a/tests/end_to_end/scenarios/get_irc_connection_info_fixed.py b/tests/end_to_end/scenarios/get_irc_connection_info_fixed.py
new file mode 100644
index 0000000..d9be151
--- /dev/null
+++ b/tests/end_to_end/scenarios/get_irc_connection_info_fixed.py
@@ -0,0 +1,17 @@
+from scenarios import *
+
+conf = 'fixed_server'
+
+scenario = (
+ send_stanza("<iq type='set' id='command1' from='{jid_one}/{resource_one}' to='{biboumi_host}'><command xmlns='http://jabber.org/protocol/commands' node='get-irc-connection-info' action='execute' /></iq>"),
+ expect_stanza("/iq/commands:command/commands:note[text()='You are not connected to the IRC server irc.localhost']"),
+
+ send_stanza("<presence from='{jid_one}/{resource_one}' to='#foo@{biboumi_host}/{nick_one}' />"),
+ sequences.connection("irc.localhost", '{jid_one}/{resource_one}', fixed_irc_server=True),
+ expect_stanza("/message"),
+ expect_stanza("/presence"),
+ expect_stanza("/message"),
+
+ send_stanza("<iq type='set' id='command2' from='{jid_one}/{resource_one}' to='{biboumi_host}'><command xmlns='http://jabber.org/protocol/commands' node='get-irc-connection-info' action='execute' /></iq>"),
+ expect_stanza(r"/iq/commands:command/commands:note[re:test(text(), 'Connected to IRC server irc.localhost on port 6667 since \d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d \(\d+ seconds ago\)\.\n#foo from 1 resource: {resource_one}.*')]"),
+)
diff --git a/tests/end_to_end/scenarios/global_configure.py b/tests/end_to_end/scenarios/global_configure.py
new file mode 100644
index 0000000..d7771c4
--- /dev/null
+++ b/tests/end_to_end/scenarios/global_configure.py
@@ -0,0 +1,27 @@
+from scenarios import *
+
+scenario = (
+ send_stanza("<iq type='set' id='id1' from='{jid_one}/{resource_one}' to='{biboumi_host}'><command xmlns='http://jabber.org/protocol/commands' node='configure' action='execute' /></iq>"),
+ expect_stanza("/iq[@type='result']/commands:command[@node='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:complete",
+ after = save_value("sessionid", extract_attribute("/iq[@type='result']/commands:command[@node='configure']", "sessionid"))),
+ send_stanza("<iq type='set' id='id2' from='{jid_one}/{resource_one}' to='{biboumi_host}'><command xmlns='http://jabber.org/protocol/commands' node='configure' sessionid='{sessionid}' action='complete'><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>"),
+ expect_stanza("/iq[@type='result']/commands:command[@node='configure'][@status='completed']/commands:note[@type='info'][text()='Configuration successfully applied.']"),
+
+ send_stanza("<iq type='set' id='id3' from='{jid_one}/{resource_one}' to='{biboumi_host}'><command xmlns='http://jabber.org/protocol/commands' node='configure' action='execute' /></iq>"),
+ expect_stanza("/iq[@type='result']/commands:command[@node='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:complete",
+ after = save_value("sessionid", extract_attribute("/iq[@type='result']/commands:command[@node='configure']", "sessionid"))),
+ 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>"),
+ expect_stanza("/iq[@type='result']/commands:command[@node='configure'][@status='canceled']"),
+)
diff --git a/tests/end_to_end/scenarios/global_configure_fixed.py b/tests/end_to_end/scenarios/global_configure_fixed.py
new file mode 100644
index 0000000..8df70ad
--- /dev/null
+++ b/tests/end_to_end/scenarios/global_configure_fixed.py
@@ -0,0 +1,32 @@
+from scenarios import *
+
+conf = 'fixed_server'
+
+scenario = (
+ 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>"),
+ 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:complete",
+ after = save_value("sessionid", extract_attribute("/iq[@type='result']/commands:command[@node='global-configure']", "sessionid"))),
+ 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='complete'><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>"),
+ expect_stanza("/iq[@type='result']/commands:command[@node='global-configure'][@status='completed']/commands:note[@type='info'][text()='Configuration successfully applied.']"),
+
+ 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>"),
+ 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:complete",
+ after = save_value("sessionid", extract_attribute("/iq[@type='result']/commands:command[@node='global-configure']", "sessionid"))),
+ 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>"),
+ expect_stanza("/iq[@type='result']/commands:command[@node='global-configure'][@status='canceled']"),
+
+ 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>"),
+ expect_stanza("/iq[@type='result']/commands:command[@node='server-configure'][@sessionid][@status='executing']"),
+)
diff --git a/tests/end_to_end/scenarios/global_configure_persistent_by_default.py b/tests/end_to_end/scenarios/global_configure_persistent_by_default.py
new file mode 100644
index 0000000..db47e88
--- /dev/null
+++ b/tests/end_to_end/scenarios/global_configure_persistent_by_default.py
@@ -0,0 +1,15 @@
+from scenarios import *
+
+conf='persistent_by_default'
+
+scenario = (
+ send_stanza("<iq type='set' id='id1' from='{jid_one}/{resource_one}' to='{biboumi_host}'><command xmlns='http://jabber.org/protocol/commands' node='configure' action='execute' /></iq>"),
+ expect_stanza("/iq[@type='result']/commands:command[@node='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()='true']",
+ "/iq/commands:command/commands:actions/commands:complete",
+ ),
+)
diff --git a/tests/end_to_end/scenarios/invite_other.py b/tests/end_to_end/scenarios/invite_other.py
new file mode 100644
index 0000000..2badf77
--- /dev/null
+++ b/tests/end_to_end/scenarios/invite_other.py
@@ -0,0 +1,21 @@
+from scenarios import *
+
+scenario = (
+ send_stanza("<presence from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}/{nick_one}' />"),
+ sequences.connection("irc.localhost", '{jid_one}/{resource_one}'),
+ expect_stanza("/message"),
+ expect_stanza("/presence"),
+ expect_stanza("/message"),
+
+ send_stanza("<presence from='{jid_two}/{resource_two}' to='#bar%{irc_server_one}/{nick_two}' />"),
+ sequences.connection("irc.localhost", '{jid_two}/{resource_two}'),
+ expect_stanza("/message"),
+ expect_stanza("/presence"),
+ expect_stanza("/message"),
+ send_stanza("<message from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}'><x xmlns='http://jabber.org/protocol/muc#user'><invite to='{nick_two}'/></x></message>"),
+ expect_stanza("/message/body[text()='{nick_two} has been invited to #foo']"),
+ expect_stanza("/message[@to='{jid_two}/{resource_two}'][@from='#foo%{irc_server_one}']/muc_user:x/muc_user:invite[@from='#foo%{irc_server_one}/{nick_one}']"),
+
+ 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>"),
+ expect_stanza("/message[@to='bertrand@example.com'][@from='#foo%{irc_server_one}']/muc_user:x/muc_user:invite[@from='{jid_one}/{resource_one}']"),
+)
diff --git a/tests/end_to_end/scenarios/irc_channel_configure.py b/tests/end_to_end/scenarios/irc_channel_configure.py
new file mode 100644
index 0000000..dcc78db
--- /dev/null
+++ b/tests/end_to_end/scenarios/irc_channel_configure.py
@@ -0,0 +1,35 @@
+from scenarios import *
+
+scenario = (
+ 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>"),
+ 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 = save_value("sessionid", extract_attribute("/iq[@type='result']/commands:command[@node='configure']", "sessionid"))
+ ),
+ send_stanza("<iq type='set' id='id2' from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}'>"
+ "<command xmlns='http://jabber.org/protocol/commands' node='configure' sessionid='{sessionid}' action='complete'>"
+ "<x xmlns='jabber:x:data' type='submit'>"
+ "<field var='ports' />"
+ "<field var='encoding_out'><value>UTF-8</value></field>"
+ "<field var='encoding_in'><value>latin-1</value></field>"
+ "<field var='record_history'><value>true</value></field>"
+ "</x></command></iq>"),
+ expect_stanza("/iq[@type='result']/commands:command[@node='configure'][@status='completed']/commands:note[@type='info'][text()='Configuration successfully applied.']"),
+
+ send_stanza("<iq type='set' id='id3' from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}'><command xmlns='http://jabber.org/protocol/commands' node='configure' action='execute' /></iq>"),
+ 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 channel #foo on server irc.localhost']",
+ "/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/dataform:x[@type='form']/dataform:field[@type='list-single'][@var='record_history']/dataform:value[text()='true']",
+ "/iq/commands:command/commands:actions/commands:complete",
+
+ after = save_value("sessionid", extract_attribute("/iq[@type='result']/commands:command[@node='configure']", "sessionid"))
+ ),
+ send_stanza("<iq type='set' id='id4' from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}'><command xmlns='http://jabber.org/protocol/commands' action='cancel' node='configure' sessionid='{sessionid}' /></iq>"),
+ expect_stanza("/iq[@type='result']/commands:command[@node='configure'][@status='canceled']"),
+)
diff --git a/tests/end_to_end/scenarios/irc_channel_configure_fixed.py b/tests/end_to_end/scenarios/irc_channel_configure_fixed.py
new file mode 100644
index 0000000..4f18c83
--- /dev/null
+++ b/tests/end_to_end/scenarios/irc_channel_configure_fixed.py
@@ -0,0 +1,33 @@
+from scenarios import *
+
+conf = 'fixed_server'
+
+scenario = (
+ send_stanza("<iq type='set' id='id1' from='{jid_one}/{resource_one}' to='#foo@{biboumi_host}'><command xmlns='http://jabber.org/protocol/commands' node='configure' action='execute' /></iq>"),
+ 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']",
+
+ after = save_value("sessionid", extract_attribute("/iq[@type='result']/commands:command[@node='configure']", "sessionid"))
+ ),
+ send_stanza("<iq type='set' id='id2' from='{jid_one}/{resource_one}' to='#foo@{biboumi_host}'>"
+ "<command xmlns='http://jabber.org/protocol/commands' node='configure' sessionid='{sessionid}' action='complete'>"
+ "<x xmlns='jabber:x:data' type='submit'>"
+ "<field var='ports' />"
+ "<field var='encoding_out'><value>UTF-8</value></field>"
+ "<field var='encoding_in'><value>latin-1</value></field>"
+ "</x></command></iq>"),
+ expect_stanza("/iq[@type='result']/commands:command[@node='configure'][@status='completed']/commands:note[@type='info'][text()='Configuration successfully applied.']"),
+
+ send_stanza("<iq type='set' id='id3' from='{jid_one}/{resource_one}' to='#foo@{biboumi_host}'><command xmlns='http://jabber.org/protocol/commands' node='configure' action='execute' /></iq>"),
+ 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 channel #foo on server irc.localhost']",
+ "/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:complete",
+
+ after = save_value("sessionid", extract_attribute("/iq[@type='result']/commands:command[@node='configure']", "sessionid"))
+ ),
+ send_stanza("<iq type='set' id='id4' from='{jid_one}/{resource_one}' to='#foo@{biboumi_host}'><command xmlns='http://jabber.org/protocol/commands' action='cancel' node='configure' sessionid='{sessionid}' /></iq>"),
+ expect_stanza("/iq[@type='result']/commands:command[@node='configure'][@status='canceled']"),
+)
diff --git a/tests/end_to_end/scenarios/irc_channel_configure_xep0045.py b/tests/end_to_end/scenarios/irc_channel_configure_xep0045.py
new file mode 100644
index 0000000..c19990d
--- /dev/null
+++ b/tests/end_to_end/scenarios/irc_channel_configure_xep0045.py
@@ -0,0 +1,20 @@
+from scenarios import *
+
+scenario = (
+ send_stanza("<iq type='get' id='id1' from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}'><query xmlns='http://jabber.org/protocol/muc#owner'/></iq>"),
+ expect_stanza("/iq[@type='result']/muc_owner:query",
+ "/iq/muc_owner:query/dataform:x[@type='form']/dataform:field[@type='text-single'][@var='encoding_in']",
+ "/iq/muc_owner:query/dataform:x[@type='form']/dataform:field[@type='text-single'][@var='encoding_out']",
+
+ ),
+ send_stanza("<iq type='set' id='id2' from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}'>"
+ "<query xmlns='http://jabber.org/protocol/muc#owner'>"
+ "<x xmlns='jabber:x:data' type='submit'>"
+ "<field var='ports' />"
+ "<field var='encoding_out'><value>UTF-8</value></field>"
+ "<field var='encoding_in'><value>latin-1</value></field>"
+ "</x></query></iq>"),
+ expect_stanza("/iq[@type='result']"),
+ send_stanza("<iq type='set' id='id3' from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}'><query xmlns='http://jabber.org/protocol/muc#owner'> <x xmlns='jabber:x:data' type='cancel'/></query></iq>"),
+ expect_stanza("/iq[@type='result']"),
+)
diff --git a/tests/end_to_end/scenarios/irc_server_configure.py b/tests/end_to_end/scenarios/irc_server_configure.py
new file mode 100644
index 0000000..1470e6e
--- /dev/null
+++ b/tests/end_to_end/scenarios/irc_server_configure.py
@@ -0,0 +1,105 @@
+from scenarios import *
+
+scenario = (
+ 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>"),
+ 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[@type='form']/dataform:field[@type='text-multi'][@var='ports']/dataform:value[text()='6667']",
+ "/iq/commands:command/dataform:x[@type='form']/dataform:field[@type='text-multi'][@var='tls_ports']/dataform:value[text()='6670']",
+ "/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-multi'][@var='after_connect_commands']",
+ "/iq/commands:command/dataform:x[@type='form']/dataform:field[@type='text-single'][@var='max_history_length']",
+ "/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']",
+ "/iq/commands:command/dataform:x[@type='form']/dataform:field[@type='text-single'][@var='encoding_out']",
+ "/iq/commands:command/commands:actions/commands:complete",
+ after = save_value("sessionid", extract_attribute("/iq[@type='result']/commands:command[@node='configure']", "sessionid"))
+ ),
+ 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='complete'>"
+ "<x xmlns='jabber:x:data' type='submit'>"
+ "<field var='ports' />"
+ "<field var='tls_ports'><value>6697</value><value>6698</value></field>"
+ "<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_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='max_history_length'><value>69</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>"
+ "</x></command></iq>"),
+ expect_stanza("/iq[@type='result']/commands:command[@node='configure'][@status='completed']/commands:note[@type='info'][text()='Configuration successfully applied.']"),
+
+ send_stanza("<iq type='set' id='id3' from='{jid_one}/{resource_one}' to='{irc_server_one}'><command xmlns='http://jabber.org/protocol/commands' node='configure' action='execute' /></iq>"),
+ 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[@type='form']/dataform:field[@type='text-multi']",
+ "/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='text-multi'][@var='tls_ports']/dataform:value[text()='6698']",
+ "/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='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='max_history_length']/dataform:value[text()='69']",
+ "/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:complete",
+ after = save_value("sessionid", extract_attribute("/iq[@type='result']/commands:command[@node='configure']", "sessionid"))
+ ),
+ send_stanza("<iq type='set' id='id4' from='{jid_one}/{resource_one}' to='{irc_server_one}'><command xmlns='http://jabber.org/protocol/commands' action='cancel' node='configure' sessionid='{sessionid}' /></iq>"),
+ expect_stanza("/iq[@type='result']/commands:command[@node='configure'][@status='canceled']"),
+
+ # Same thing, but try to empty some values
+ 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>"),
+ expect_stanza("/iq[@type='result']",
+ after = save_value("sessionid", extract_attribute("/iq[@type='result']/commands:command[@node='configure']", "sessionid"))
+ ),
+ 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='complete'>"
+ "<x xmlns='jabber:x:data' type='submit'>"
+ "<field var='pass'><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>"),
+ expect_stanza("/iq[@type='result']/commands:command[@node='configure'][@status='completed']/commands:note[@type='info'][text()='Configuration successfully applied.']"),
+
+ send_stanza("<iq type='set' id='id3' from='{jid_one}/{resource_one}' to='{irc_server_one}'><command xmlns='http://jabber.org/protocol/commands' node='configure' action='execute' /></iq>"),
+ 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_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:complete",
+ "/iq/commands:command/dataform:x[@type='form']/dataform:field[@type='text-single'][@var='throttle_limit']/dataform:value[text()='10']", # An invalid value sets this field to its default value
+ after = save_value("sessionid", extract_attribute("/iq[@type='result']/commands:command[@node='configure']", "sessionid"))
+ ),
+ send_stanza("<iq type='set' id='id4' from='{jid_one}/{resource_one}' to='{irc_server_one}'><command xmlns='http://jabber.org/protocol/commands' action='cancel' node='configure' sessionid='{sessionid}' /></iq>"),
+ expect_stanza("/iq[@type='result']/commands:command[@node='configure'][@status='canceled']"),
+
+ )
diff --git a/tests/end_to_end/scenarios/irc_server_connection.py b/tests/end_to_end/scenarios/irc_server_connection.py
new file mode 100644
index 0000000..25ea749
--- /dev/null
+++ b/tests/end_to_end/scenarios/irc_server_connection.py
@@ -0,0 +1,7 @@
+from scenarios import *
+
+scenario = (
+ send_stanza("<presence from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}/{nick_one}' />"),
+ sequences.connection(),
+ )
+
diff --git a/tests/end_to_end/scenarios/irc_server_connection_failure.py b/tests/end_to_end/scenarios/irc_server_connection_failure.py
new file mode 100644
index 0000000..2abe39f
--- /dev/null
+++ b/tests/end_to_end/scenarios/irc_server_connection_failure.py
@@ -0,0 +1,11 @@
+from scenarios import *
+
+scenario = (
+ send_stanza("<presence from='{jid_one}/{resource_one}' to='#foo%doesnotexist@{biboumi_host}/{nick_one}' />"),
+ expect_stanza("/message/body[text()='Connecting to doesnotexist:6697 (encrypted)']"),
+ expect_stanza("/message/body[re:test(text(), 'Connection failed: (Domain name not found|Name or service not known)')]"),
+ expect_stanza("/presence[@from='#foo%doesnotexist@{biboumi_host}/{nick_one}']/muc:x",
+ "/presence/error[@type='cancel']/stanza:item-not-found",
+ "/presence/error[@type='cancel']/stanza:text[re:test(text(), '(Domain name not found|Name or service not known)')]",
+ ),
+)
diff --git a/tests/end_to_end/scenarios/irc_server_presence_in_roster.py b/tests/end_to_end/scenarios/irc_server_presence_in_roster.py
new file mode 100644
index 0000000..bc2016d
--- /dev/null
+++ b/tests/end_to_end/scenarios/irc_server_presence_in_roster.py
@@ -0,0 +1,25 @@
+from scenarios import *
+
+scenario = (
+ # Mutual subscription exchange
+ send_stanza("<presence from='{jid_one}' to='{irc_server_one}' type='subscribe' id='subid1' />"),
+ expect_stanza("/presence[@type='subscribed'][@id='subid1']"),
+
+ expect_stanza("/presence[@type='subscribe']"),
+ send_stanza("<presence from='{jid_one}' to='{irc_server_one}' type='subscribed' />"),
+
+ # Join a channel on that server
+ send_stanza("<presence from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}/{nick_one}' />"),
+
+ # We must receive the IRC server presence, in the connection sequence
+ sequences.connection("irc.localhost", '{jid_one}/{resource_one}', expected_irc_presence=True),
+ expect_stanza("/message/body[text()='Mode #foo [+nt] by {irc_host_one}']"),
+ 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']"),
+ expect_stanza("/message[@from='#foo%{irc_server_one}'][@type='groupchat']/subject[not(text())]"),
+
+ # Leave the channel, and thus the IRC server
+ send_stanza("<presence type='unavailable' from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}/{nick_one}' />"),
+ expect_stanza("/presence[@type='unavailable'][@from='#foo%{irc_server_one}/{nick_one}']"),
+ expect_stanza("/presence[@from='{irc_server_one}'][@to='{jid_one}'][@type='unavailable']"),
+)
diff --git a/tests/end_to_end/scenarios/irc_server_presence_subscription.py b/tests/end_to_end/scenarios/irc_server_presence_subscription.py
new file mode 100644
index 0000000..e9ad1a5
--- /dev/null
+++ b/tests/end_to_end/scenarios/irc_server_presence_subscription.py
@@ -0,0 +1,6 @@
+from scenarios import *
+
+scenario = (
+ send_stanza("<presence type='subscribe' from='{jid_one}/{resource_one}' to='{irc_server_one}' id='sub1' />"),
+ expect_stanza("/presence[@to='{jid_one}'][@from='{irc_server_one}'][@type='subscribed']"),
+)
diff --git a/tests/end_to_end/scenarios/irc_tls_connection.py b/tests/end_to_end/scenarios/irc_tls_connection.py
new file mode 100644
index 0000000..8b30893
--- /dev/null
+++ b/tests/end_to_end/scenarios/irc_tls_connection.py
@@ -0,0 +1,25 @@
+from scenarios import *
+
+scenario = (
+ # First, use an adhoc command to configure how we connect to the irc server, configure
+ # only one TLS port, and disable the cert verification.
+ 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>"),
+ expect_stanza("/iq[@type='result']",
+ after = save_value("sessionid", extract_attribute("/iq[@type='result']/commands:command[@node='configure']", "sessionid"))),
+ 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' />"
+ "<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>"),
+ expect_stanza("/iq[@type='result']/commands:command[@node='configure'][@status='completed']/commands:note[@type='info'][text()='Configuration successfully applied.']"),
+
+ send_stanza("<presence from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}/{nick_one}' />"),
+ sequences.connection_tls("irc.localhost", '{jid_one}/{resource_one}'),
+ expect_stanza("/message/body[text()='Mode #foo [+nt] by {irc_host_one}']"),
+ expect_stanza("/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']"),
+ expect_stanza("/message[@from='#foo%{irc_server_one}'][@type='groupchat']/subject[not(text())]"),
+)
diff --git a/tests/end_to_end/scenarios/join_history_limit.py b/tests/end_to_end/scenarios/join_history_limit.py
new file mode 100644
index 0000000..2432f14
--- /dev/null
+++ b/tests/end_to_end/scenarios/join_history_limit.py
@@ -0,0 +1,108 @@
+from scenarios import *
+
+scenario = (
+ # Disable the throttling because the test is based on timings
+ 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>"),
+ expect_stanza("/iq[@type='result']",
+ after = save_value("sessionid", extract_attribute("/iq[@type='result']/commands:command[@node='configure']", "sessionid"))),
+ 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>"),
+ expect_stanza("/iq[@type='result']/commands:command[@node='configure'][@status='completed']/commands:note[@type='info'][text()='Configuration successfully applied.']"),
+
+
+ send_stanza("<presence from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}/{nick_one}' />"),
+ sequences.connection("irc.localhost", '{jid_one}/{resource_one}'),
+ expect_stanza("/message/body[text()='Mode #foo [+nt] by {irc_host_one}']"),
+ 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']"),
+ expect_stanza("/message[@from='#foo%{irc_server_one}'][@type='groupchat']/subject[not(text())]"),
+
+ # Send two channel messages
+ send_stanza("<message from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}' type='groupchat'><body>coucou</body></message>"),
+ 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_stanza("<message from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}' type='groupchat'><body>coucou 2</body></message>"),
+ # Record the current time
+ expect_stanza("/message[@from='#foo%{irc_server_one}/{nick_one}'][@to='{jid_one}/{resource_one}'][@type='groupchat']/body[text()='coucou 2']",
+ after = save_current_timestamp_plus_delta("first_timestamp", datetime.timedelta(seconds=1))),
+
+ # Wait two seconds before sending two new messages
+ sleep_for(2),
+ send_stanza("<message from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}' type='groupchat'><body>coucou 3</body></message>"),
+ send_stanza("<message from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}' type='groupchat'><body>coucou 4</body></message>"),
+ expect_stanza("/message[@type='groupchat']/body[text()='coucou 3']"),
+ expect_stanza("/message[@type='groupchat']/body[text()='coucou 4']",
+ after = save_current_timestamp_plus_delta("second_timestamp", datetime.timedelta(seconds=1))),
+
+ # join some other channel, to stay connected to the server even after leaving #foo
+ send_stanza("<presence from='{jid_one}/{resource_one}' to='#DUMMY%{irc_server_one}/{nick_one}' />"),
+ expect_stanza("/message"),
+ expect_stanza("/presence/muc_user:x/muc_user:status[@code='110']"),
+ expect_stanza("/message/subject"),
+
+ # Leave #foo
+ send_stanza("<presence from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}' type='unavailable' />"),
+ expect_stanza("/presence[@type='unavailable']"),
+
+ sleep_for(0.2),
+
+ # Rejoin #foo, with some history limit
+ 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>"),
+ expect_stanza("/message"),
+ expect_stanza("/presence/muc_user:x/muc_user:status[@code='110']"),
+ expect_stanza("/message/subject"),
+
+ send_stanza("<presence from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}' type='unavailable' />"),
+ expect_stanza("/presence[@type='unavailable']"),
+
+ sleep_for(0.2),
+
+ # Rejoin #foo, with some history limit
+ 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>"),
+ expect_stanza("/message"),
+ expect_stanza("/presence/muc_user:x/muc_user:status[@code='110']"),
+ expect_stanza("/message[@from='#foo%{irc_server_one}/{nick_one}'][@type='groupchat']/body[text()='coucou 2']"),
+ expect_stanza("/message[@from='#foo%{irc_server_one}/{nick_one}'][@type='groupchat']/body[text()='coucou 3']"),
+ expect_stanza("/message[@from='#foo%{irc_server_one}/{nick_one}'][@type='groupchat']/body[text()='coucou 4']"),
+ expect_stanza("/message/subject"),
+
+ send_stanza("<presence from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}' type='unavailable' />"),
+ expect_stanza("/presence[@type='unavailable']"),
+
+ # Rejoin #foo, with some history limit
+ send_stanza("<presence from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}/{nick_one}'><x xmlns='http://jabber.org/protocol/muc'><history since='{first_timestamp}'/></x></presence>"),
+ expect_stanza("/message"),
+ expect_stanza("/presence/muc_user:x/muc_user:status[@code='110']"),
+ expect_stanza("/message[@from='#foo%{irc_server_one}/{nick_one}'][@type='groupchat']/body[text()='coucou 3']"),
+ expect_stanza("/message[@from='#foo%{irc_server_one}/{nick_one}'][@type='groupchat']/body[text()='coucou 4']"),
+ expect_stanza("/message/subject"),
+ send_stanza("<presence from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}' type='unavailable' />"),
+ expect_stanza("/presence[@type='unavailable']"),
+
+ # Rejoin #foo, with some history limit
+ send_stanza("<presence from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}/{nick_one}'><x xmlns='http://jabber.org/protocol/muc'><history seconds='1'/></x></presence>"),
+ expect_stanza("/message"),
+ expect_stanza("/presence/muc_user:x/muc_user:status[@code='110']"),
+ expect_stanza("/message[@from='#foo%{irc_server_one}/{nick_one}'][@type='groupchat']/body[text()='coucou 3']"),
+ expect_stanza("/message[@from='#foo%{irc_server_one}/{nick_one}'][@type='groupchat']/body[text()='coucou 4']"),
+ expect_stanza("/message/subject"),
+ send_stanza("<presence from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}' type='unavailable' />"),
+ expect_stanza("/presence[@type='unavailable']"),
+
+ # Rejoin #foo, with some history limit
+ send_stanza("<presence from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}/{nick_one}'><x xmlns='http://jabber.org/protocol/muc'><history seconds='5'/></x></presence>"),
+ expect_stanza("/message"),
+ expect_stanza("/presence/muc_user:x/muc_user:status[@code='110']"),
+ expect_stanza("/message[@from='#foo%{irc_server_one}/{nick_one}'][@type='groupchat']/body[text()='coucou']"), expect_stanza("/message[@from='#foo%{irc_server_one}/{nick_one}'][@type='groupchat']/body[text()='coucou 2']"),
+ expect_stanza("/message[@from='#foo%{irc_server_one}/{nick_one}'][@type='groupchat']/body[text()='coucou 3']"),
+ expect_stanza("/message[@from='#foo%{irc_server_one}/{nick_one}'][@type='groupchat']/body[text()='coucou 4']"),
+ expect_stanza("/message/subject"),
+ send_stanza("<presence from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}' type='unavailable' />"),
+ expect_stanza("/presence[@type='unavailable']"),
+)
diff --git a/tests/end_to_end/scenarios/leave_unjoined_chan.py b/tests/end_to_end/scenarios/leave_unjoined_chan.py
new file mode 100644
index 0000000..a9751d7
--- /dev/null
+++ b/tests/end_to_end/scenarios/leave_unjoined_chan.py
@@ -0,0 +1,16 @@
+from scenarios import *
+
+scenario = (
+ send_stanza("<presence from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}/{nick_one}' />"),
+ sequences.connection("irc.localhost", '{jid_one}/{resource_one}'),
+ expect_stanza("/message"),
+ expect_stanza("/presence"),
+ expect_stanza("/message"),
+
+ send_stanza("<presence from='{jid_two}/{resource_two}' to='#foo%{irc_server_one}/{nick_one}' />"),
+ sequences.connection_begin("irc.localhost", '{jid_two}/{resource_two}'),
+
+ expect_stanza("/message[@to='{jid_two}/{resource_two}'][@type='chat']/body[text()='irc.localhost: {nick_one}: Nickname is already in use.']"),
+ expect_stanza("/presence[@type='error']/error[@type='cancel'][@code='409']/stanza:conflict"),
+ send_stanza("<presence from='{jid_two}/{resource_two}' to='#foo%{irc_server_one}/{nick_one}' type='unavailable' />")
+)
diff --git a/tests/end_to_end/scenarios/list_adhoc.py b/tests/end_to_end/scenarios/list_adhoc.py
new file mode 100644
index 0000000..7b46312
--- /dev/null
+++ b/tests/end_to_end/scenarios/list_adhoc.py
@@ -0,0 +1,10 @@
+from scenarios import *
+
+scenario = (
+ 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>"),
+ expect_stanza("/iq[@type='result']/disco_items:query[@node='http://jabber.org/protocol/commands']",
+ "/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]"),
+
+)
diff --git a/tests/end_to_end/scenarios/list_adhoc_fixed_server.py b/tests/end_to_end/scenarios/list_adhoc_fixed_server.py
new file mode 100644
index 0000000..fef378b
--- /dev/null
+++ b/tests/end_to_end/scenarios/list_adhoc_fixed_server.py
@@ -0,0 +1,12 @@
+from scenarios import *
+
+conf = "fixed_server"
+
+scenario = (
+ 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>"),
+ expect_stanza("/iq[@type='result']/disco_items:query[@node='http://jabber.org/protocol/commands']",
+ "/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]"),
+)
diff --git a/tests/end_to_end/scenarios/list_adhoc_irc.py b/tests/end_to_end/scenarios/list_adhoc_irc.py
new file mode 100644
index 0000000..ff94a1b
--- /dev/null
+++ b/tests/end_to_end/scenarios/list_adhoc_irc.py
@@ -0,0 +1,8 @@
+from scenarios import *
+
+scenario = (
+ send_stanza("<iq type='get' id='idwhatever' from='{jid_one}/{resource_one}' to='{irc_host_one}@{biboumi_host}'><query xmlns='http://jabber.org/protocol/disco#items' node='http://jabber.org/protocol/commands' /></iq>"),
+ expect_stanza("/iq[@type='result']/disco_items:query[@node='http://jabber.org/protocol/commands']",
+ "/iq/disco_items:query/disco_items:item[2]",
+ "!/iq/disco_items:query/disco_items:item[3]"),
+)
diff --git a/tests/end_to_end/scenarios/list_admin_adhoc.py b/tests/end_to_end/scenarios/list_admin_adhoc.py
new file mode 100644
index 0000000..0b71662
--- /dev/null
+++ b/tests/end_to_end/scenarios/list_admin_adhoc.py
@@ -0,0 +1,9 @@
+from scenarios import *
+
+scenario = (
+ 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>"),
+ expect_stanza("/iq[@type='result']/disco_items:query[@node='http://jabber.org/protocol/commands']",
+ "/iq/disco_items:query/disco_items:item[@node='configure']",
+ "/iq/disco_items:query/disco_items:item[6]",
+ "!/iq/disco_items:query/disco_items:item[7]"),
+)
diff --git a/tests/end_to_end/scenarios/list_admin_adhoc_fixed_server.py b/tests/end_to_end/scenarios/list_admin_adhoc_fixed_server.py
new file mode 100644
index 0000000..8e2775e
--- /dev/null
+++ b/tests/end_to_end/scenarios/list_admin_adhoc_fixed_server.py
@@ -0,0 +1,12 @@
+from scenarios import *
+
+conf = "fixed_server"
+
+scenario = (
+ 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>"),
+ expect_stanza("/iq[@type='result']/disco_items:query[@node='http://jabber.org/protocol/commands']",
+ "/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[8]",
+ "!/iq/disco_items:query/disco_items:item[9]"),
+)
diff --git a/tests/end_to_end/scenarios/list_muc_user_adhoc.py b/tests/end_to_end/scenarios/list_muc_user_adhoc.py
new file mode 100644
index 0000000..6827a8d
--- /dev/null
+++ b/tests/end_to_end/scenarios/list_muc_user_adhoc.py
@@ -0,0 +1,6 @@
+from scenarios import *
+
+scenario = (
+ send_stanza("<iq type='get' id='idwhatever' from='{jid_admin}/{resource_one}' to='#foo%{irc_server_one}/{nick_one}'><query xmlns='http://jabber.org/protocol/disco#items' node='http://jabber.org/protocol/commands' /></iq>"),
+ expect_stanza("/iq[@type='error']/error[@type='cancel']/stanza:feature-not-implemented"),
+)
diff --git a/tests/end_to_end/scenarios/mam_on_fixed_server.py b/tests/end_to_end/scenarios/mam_on_fixed_server.py
new file mode 100644
index 0000000..200f04e
--- /dev/null
+++ b/tests/end_to_end/scenarios/mam_on_fixed_server.py
@@ -0,0 +1,21 @@
+from scenarios import *
+
+conf = 'fixed_server'
+
+scenario = (
+ scenarios.channel_join_on_fixed_irc_server.scenario,
+
+ send_stanza("<message from='{jid_one}/{resource_one}' to='#foo@{biboumi_host}' type='groupchat'><body>coucou</body></message>"),
+ expect_stanza("/message[@from='#foo@{biboumi_host}/{nick_one}'][@to='{jid_one}/{resource_one}'][@type='groupchat']/body[text()='coucou']"),
+
+ send_stanza("<message from='{jid_one}/{resource_one}' to='#foo@{biboumi_host}' type='groupchat'><body>coucou 2</body></message>"),
+ expect_stanza("/message[@from='#foo@{biboumi_host}/{nick_one}'][@to='{jid_one}/{resource_one}'][@type='groupchat']/body[text()='coucou 2']"),
+
+ # Retrieve the complete archive
+ send_stanza("<iq to='#foo@{biboumi_host}' from='{jid_one}/{resource_one}' type='set' id='id1'><query xmlns='urn:xmpp:mam:2' queryid='qid1' /></iq>"),
+
+ expect_stanza("/message/mam:result[@queryid='qid1']/forward:forwarded/delay:delay",
+ "/message/mam:result[@queryid='qid1']/forward:forwarded/client:message[@from='#foo@{biboumi_host}/{nick_one}'][@type='groupchat']/client:body[text()='coucou']"),
+ expect_stanza("/message/mam:result[@queryid='qid1']/forward:forwarded/delay:delay",
+ "/message/mam:result[@queryid='qid1']/forward:forwarded/client:message[@from='#foo@{biboumi_host}/{nick_one}'][@type='groupchat']/client:body[text()='coucou 2']"),
+)
diff --git a/tests/end_to_end/scenarios/mam_with_timestamps.py b/tests/end_to_end/scenarios/mam_with_timestamps.py
new file mode 100644
index 0000000..0ed0333
--- /dev/null
+++ b/tests/end_to_end/scenarios/mam_with_timestamps.py
@@ -0,0 +1,63 @@
+from scenarios import *
+
+scenario = (
+ scenarios.simple_channel_join.scenario,
+
+ # Send two channel messages
+ send_stanza("<message from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}' type='groupchat'><body>coucou</body></message>"),
+ 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_stanza("<message from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}' type='groupchat'><body>coucou 2</body></message>"),
+ # Record the current time
+ expect_stanza("/message[@from='#foo%{irc_server_one}/{nick_one}'][@to='{jid_one}/{resource_one}'][@type='groupchat']/body[text()='coucou 2']",
+ after = save_current_timestamp_plus_delta("first_timestamp", datetime.timedelta(seconds=1))),
+
+ # Wait two seconds before sending two new messages
+ sleep_for(2),
+ send_stanza("<message from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}' type='groupchat'><body>coucou 3</body></message>"),
+ send_stanza("<message from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}' type='groupchat'><body>coucou 4</body></message>"),
+ expect_stanza("/message[@type='groupchat']/body[text()='coucou 3']"),
+ expect_stanza("/message[@type='groupchat']/body[text()='coucou 4']",
+ after = save_current_timestamp_plus_delta("second_timestamp", datetime.timedelta(seconds=1))),
+
+ # Retrieve the archive, after our saved datetime
+ send_stanza("""<iq to='#foo%{irc_server_one}' from='{jid_one}/{resource_one}' type='set' id='id8'>
+ <query xmlns='urn:xmpp:mam:2' queryid='qid16'>
+ <x type='submit' xmlns='jabber:x:data'>
+ <field var='FORM_TYPE' xmlns='jabber:x:data'><value xmlns='jabber:x:data'>urn:xmpp:mam:2</value></field>
+ <field var='start' xmlns='jabber:x:data'><value xmlns='jabber:x:data'>{first_timestamp}</value></field>
+ <field var='end' xmlns='jabber:x:data'><value xmlns='jabber:x:data'>{second_timestamp}</value></field>
+ </x>
+ </query>
+ </iq>"""),
+
+ expect_stanza("/message/mam:result[@queryid='qid16']/forward:forwarded/delay:delay",
+ "/message/mam:result/forward:forwarded/client:message[@from='#foo%{irc_server_one}/{nick_one}'][@type='groupchat']/client:body[text()='coucou 3']"),
+
+ expect_stanza("/message/mam:result[@queryid='qid16']/forward:forwarded/delay:delay",
+ "/message/mam:result/forward:forwarded/client:message[@from='#foo%{irc_server_one}/{nick_one}'][@type='groupchat']/client:body[text()='coucou 4']"),
+
+ expect_stanza("/iq[@type='result'][@id='id8'][@from='#foo%{irc_server_one}'][@to='{jid_one}/{resource_one}']",
+ "/iq/mam:fin[@complete='true']/rsm:set"),
+
+ # Try the same thing, but only with the 'start' value, omitting the end
+ send_stanza("""<iq from='{jid_one}/{resource_one}' id='id888' to='#foo%{irc_server_one}' type='set'>
+ <query queryid='qid17' xmlns='urn:xmpp:mam:2'>
+ <x type='submit' xmlns='jabber:x:data'>
+ <field type='hidden' var='FORM_TYPE' xmlns='jabber:x:data'><value xmlns='jabber:x:data'>urn:xmpp:mam:2</value></field>
+ <field var='start' xmlns='jabber:x:data'><value xmlns='jabber:x:data'>{first_timestamp}</value></field>
+ </x>
+ </query>
+ </iq>"""),
+
+ expect_stanza("/message/mam:result[@queryid='qid17']/forward:forwarded/delay:delay",
+ "/message/mam:result/forward:forwarded/client:message[@from='#foo%{irc_server_one}/{nick_one}'][@type='groupchat']/client:body[text()='coucou 3']"),
+
+ expect_stanza("/message/mam:result[@queryid='qid17']/forward:forwarded/delay:delay",
+ "/message/mam:result/forward:forwarded/client:message[@from='#foo%{irc_server_one}/{nick_one}'][@type='groupchat']/client:body[text()='coucou 4']"),
+
+ expect_stanza("/iq[@type='result'][@id='id888'][@from='#foo%{irc_server_one}'][@to='{jid_one}/{resource_one}']",
+ "/iq/mam:fin[@complete='true']/rsm:set"),
+
+)
diff --git a/tests/end_to_end/scenarios/mode_change.py b/tests/end_to_end/scenarios/mode_change.py
new file mode 100644
index 0000000..4cbf036
--- /dev/null
+++ b/tests/end_to_end/scenarios/mode_change.py
@@ -0,0 +1,52 @@
+from scenarios import *
+
+scenario = (
+ scenarios.channel_join_with_two_users.scenario,
+
+ # Change a user mode with a message starting with /mode
+ send_stanza("<message from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}' type='groupchat'><body>/mode +v {nick_two}</body></message>"),
+ expect_unordered(
+ ["/message[@to='{jid_one}/{resource_one}']/body[text()='Mode #foo [+v {nick_two}] by {nick_one}']"],
+ ["/message[@to='{jid_two}/{resource_one}']/body[text()='Mode #foo [+v {nick_two}] by {nick_one}']"],
+ ["/presence[@to='{jid_two}/{resource_one}'][@from='#foo%{irc_server_one}/{nick_two}']/muc_user:x/muc_user:item[@affiliation='member'][@role='participant']"],
+ ["/presence[@to='{jid_one}/{resource_one}'][@from='#foo%{irc_server_one}/{nick_two}']/muc_user:x/muc_user:item[@affiliation='member'][@role='participant']"],
+ ),
+
+ # using an iq
+ send_stanza("<iq from='{jid_one}/{resource_one}' id='id1' to='#foo%{irc_server_one}' type='set'><query xmlns='http://jabber.org/protocol/muc#admin'><item affiliation='admin' nick='{nick_two}'/></query></iq>"),
+ expect_unordered(
+ ["/message[@to='{jid_one}/{resource_one}']/body[text()='Mode #foo [+o {nick_two}] by {nick_one}']"],
+ ["/message[@to='{jid_two}/{resource_one}']/body[text()='Mode #foo [+o {nick_two}] by {nick_one}']"],
+ ["/presence[@to='{jid_two}/{resource_one}'][@from='#foo%{irc_server_one}/{nick_two}']/muc_user:x/muc_user:item[@affiliation='admin'][@role='moderator']"],
+ ["/presence[@to='{jid_one}/{resource_one}'][@from='#foo%{irc_server_one}/{nick_two}']/muc_user:x/muc_user:item[@affiliation='admin'][@role='moderator']"],
+ ["/iq[@id='id1'][@type='result'][@to='{jid_one}/{resource_one}'][@from='#foo%{irc_server_one}']"],
+ ),
+
+ # remove the mode
+ send_stanza("<iq from='{jid_one}/{resource_one}' id='id1' to='#foo%{irc_server_one}' type='set'><query xmlns='http://jabber.org/protocol/muc#admin'><item affiliation='member' nick='{nick_two}' role='participant'/></query></iq>"),
+ expect_unordered(
+ ["/message[@to='{jid_one}/{resource_one}']/body[text()='Mode #foo [+v-o {nick_two} {nick_two}] by {nick_one}']"],
+ ["/message[@to='{jid_two}/{resource_one}']/body[text()='Mode #foo [+v-o {nick_two} {nick_two}] by {nick_one}']"],
+ ["/presence[@to='{jid_two}/{resource_one}'][@from='#foo%{irc_server_one}/{nick_two}']/muc_user:x/muc_user:item[@affiliation='member'][@role='participant']"],
+ ["/presence[@to='{jid_one}/{resource_one}'][@from='#foo%{irc_server_one}/{nick_two}']/muc_user:x/muc_user:item[@affiliation='member'][@role='participant']"],
+ ["/iq[@id='id1'][@type='result'][@to='{jid_one}/{resource_one}'][@from='#foo%{irc_server_one}']"],
+ ),
+
+ # using an iq, an a non-existant nick
+ send_stanza("<iq from='{jid_one}/{resource_one}' id='id1' to='#foo%{irc_server_one}' type='set'><query xmlns='http://jabber.org/protocol/muc#admin'><item affiliation='admin' nick='blectre'/></query></iq>"),
+ expect_stanza("/iq[@type='error']"),
+
+ # using an iq, without the rights to do it
+ send_stanza("<iq from='{jid_two}/{resource_one}' id='id1' to='#foo%{irc_server_one}' type='set'><query xmlns='http://jabber.org/protocol/muc#admin'><item affiliation='admin' nick='{nick_one}'/></query></iq>"),
+ expect_unordered(
+ ["/iq[@type='error']"],
+ ["/message[@type='chat'][@to='{jid_two}/{resource_one}']"]
+ ),
+
+ # using an iq, with an unknown mode
+ send_stanza("<iq from='{jid_two}/{resource_one}' id='id1' to='#foo%{irc_server_one}' type='set'><query xmlns='http://jabber.org/protocol/muc#admin'><item affiliation='owner' nick='{nick_one}'/></query></iq>"),
+ expect_unordered(
+ ["/iq[@type='error']"],
+ ["/message[@type='chat'][@to='{jid_two}/{resource_one}']"],
+ ),
+)
diff --git a/tests/end_to_end/scenarios/muc_disco_info.py b/tests/end_to_end/scenarios/muc_disco_info.py
new file mode 100644
index 0000000..19e90f2
--- /dev/null
+++ b/tests/end_to_end/scenarios/muc_disco_info.py
@@ -0,0 +1,28 @@
+from scenarios import *
+
+scenario = (
+ send_stanza("<iq from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}' id='1' type='get'><query xmlns='http://jabber.org/protocol/disco#info'/></iq>"),
+ 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='#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/disco_info:feature[@var='muc_nonanonymous']",
+ "!/iq/disco_info:query/dataform:x/dataform:field[@var='muc#roominfo_occupants']"),
+
+ # Join the channel, and re-do the same query
+ send_stanza("<presence from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}/{nick_one}' />"),
+ sequences.connection("irc.localhost", '{jid_one}/{resource_one}'),
+ expect_stanza("/message/body[text()='Mode #foo [+nt] by {irc_host_one}']"),
+ 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']"),
+
+ expect_stanza("/message[@from='#foo%{irc_server_one}'][@type='groupchat']/subject[not(text())]"),
+
+ 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>"),
+ 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']"),
+)
diff --git a/tests/end_to_end/scenarios/muc_message_from_unjoined_resource.py b/tests/end_to_end/scenarios/muc_message_from_unjoined_resource.py
new file mode 100644
index 0000000..bffe3aa
--- /dev/null
+++ b/tests/end_to_end/scenarios/muc_message_from_unjoined_resource.py
@@ -0,0 +1,16 @@
+from scenarios import *
+
+scenario = (
+ scenarios.simple_channel_join.scenario,
+
+ # Send a channel message
+ send_stanza("<message from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}' type='groupchat'><body>coucou</body></message>"),
+ # Receive the message
+ 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
+ send_stanza("<message from='{jid_one}/{resource_two}' to='#foo%{irc_server_one}' type='groupchat'><body>coucou</body></message>"),
+ expect_stanza("/message[@type='error']/error[@type='modify']/stanza:text[text()='You are not a participant in this room.']",
+ "/message/error/stanza:not-acceptable"
+ ),
+)
diff --git a/tests/end_to_end/scenarios/muc_traffic_info.py b/tests/end_to_end/scenarios/muc_traffic_info.py
new file mode 100644
index 0000000..0ef0d37
--- /dev/null
+++ b/tests/end_to_end/scenarios/muc_traffic_info.py
@@ -0,0 +1,6 @@
+from scenarios import *
+
+scenario = (
+ send_stanza("<iq from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}' id='1' type='get'><query xmlns='http://jabber.org/protocol/disco#info' node='http://jabber.org/protocol/muc#traffic'/></iq>"),
+ expect_stanza("/iq[@from='#foo%{irc_server_one}'][@to='{jid_one}/{resource_one}'][@type='result']/disco_info:query[@node='http://jabber.org/protocol/muc#traffic']"),
+)
diff --git a/tests/end_to_end/scenarios/multiline_message.py b/tests/end_to_end/scenarios/multiline_message.py
new file mode 100644
index 0000000..fc88e66
--- /dev/null
+++ b/tests/end_to_end/scenarios/multiline_message.py
@@ -0,0 +1,62 @@
+from scenarios import *
+
+import scenarios.simple_channel_join
+
+scenario = (
+ scenarios.simple_channel_join.scenario,
+
+ # Send a multi-line channel message
+ 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
+ expect_stanza("/message[@from='#foo%{irc_server_one}/{nick_one}'][@id='the-message-id'][@to='{jid_one}/{resource_one}'][@type='groupchat']/body[text()='un']"),
+ expect_stanza("/message[@from='#foo%{irc_server_one}/{nick_one}'][@id][@to='{jid_one}/{resource_one}'][@type='groupchat']/body[text()='deux']"),
+ 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
+ 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)
+ expect_stanza("!/message[@id='']",
+ "/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.
+ sleep_for(0.2),
+ # Second user joins
+ send_stanza("<presence from='{jid_two}/{resource_one}' to='#foo%{irc_server_one}/{nick_two}' />"),
+ sequences.connection("irc.localhost", '{jid_two}/{resource_one}'),
+ # Our presence, sent to the other user
+ expect_unordered(
+ ["/presence[@to='{jid_one}/{resource_one}'][@from='#foo%{irc_server_one}/{nick_two}']/muc_user:x/muc_user:item[@affiliation='none'][@jid='{lower_nick_two}%{irc_server_one}/~{nick_two}@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='{lower_nick_two}%{irc_server_one}/~{nick_two}@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
+ 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
+ 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']"],
+ )
+)
diff --git a/tests/end_to_end/scenarios/multiline_topic.py b/tests/end_to_end/scenarios/multiline_topic.py
new file mode 100644
index 0000000..ca163a0
--- /dev/null
+++ b/tests/end_to_end/scenarios/multiline_topic.py
@@ -0,0 +1,11 @@
+from scenarios import *
+
+import scenarios.simple_channel_join
+
+scenario = (
+ scenarios.simple_channel_join.scenario,
+ # User tries to set a multiline topic
+ send_stanza("<message from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}' type='groupchat'><subject>FIRST LINE\nSECOND LINE.</subject></message>"),
+ # Server converts the newline into spaces, because IRC can’t have them in the topic
+ expect_stanza("/message[@from='#foo%{irc_server_one}/{nick_one}'][@type='groupchat'][@to='{jid_one}/{resource_one}']/subject[text()='FIRST LINE SECOND LINE.']")
+)
diff --git a/tests/end_to_end/scenarios/multiple_channels_join.py b/tests/end_to_end/scenarios/multiple_channels_join.py
new file mode 100644
index 0000000..84e4360
--- /dev/null
+++ b/tests/end_to_end/scenarios/multiple_channels_join.py
@@ -0,0 +1,18 @@
+from scenarios import *
+
+from scenarios.simple_channel_join import expect_self_join_presence
+
+scenario = (
+ # Join 3 rooms, on the same server, with three different nicks
+ send_stanza("<presence from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}/{nick_one}' />"),
+ send_stanza("<presence from='{jid_one}/{resource_one}' to='#bar%{irc_server_one}/{nick_two}' />"),
+ send_stanza("<presence from='{jid_one}/{resource_one}' to='#baz%{irc_server_one}/{nick_three}'> <x xmlns='http://jabber.org/protocol/muc'><password>SECRET</password></x></presence>"),
+
+ sequences.connection(),
+
+ # The first nick we specified should be the only one we receive, the rest was ignored
+ expect_self_join_presence(jid='{jid_one}/{resource_one}', chan="#foo", nick="{nick_one}"),
+ expect_self_join_presence(jid='{jid_one}/{resource_one}', chan="#bar", nick="{nick_one}"),
+ expect_self_join_presence(jid='{jid_one}/{resource_one}', chan="#baz", nick="{nick_one}"),
+)
+
diff --git a/tests/end_to_end/scenarios/multisession_kick.py b/tests/end_to_end/scenarios/multisession_kick.py
new file mode 100644
index 0000000..7d8679f
--- /dev/null
+++ b/tests/end_to_end/scenarios/multisession_kick.py
@@ -0,0 +1,46 @@
+from scenarios import *
+
+scenario = (
+ scenarios.simple_channel_join.scenario,
+
+ # Second user joins, from two resources
+ send_stanza("<presence from='{jid_two}/{resource_one}' to='#foo%{irc_server_one}/{nick_two}' />"),
+ sequences.connection("irc.localhost", '{jid_two}/{resource_one}'),
+ 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']"],
+ ["/message/subject"]
+ ),
+ # Second resource
+ send_stanza("<presence from='{jid_two}/{resource_two}' to='#foo%{irc_server_one}/{nick_two}' />"),
+ expect_stanza("/presence[@to='{jid_two}/{resource_two}'][@from='#foo%{irc_server_one}/{nick_one}']"),
+ expect_stanza("/presence[@to='{jid_two}/{resource_two}'][@from='#foo%{irc_server_one}/{nick_two}']",
+ "/presence/muc_user:x/muc_user:status[@code='110']"
+ ),
+ expect_stanza("/message[@from='#foo%{irc_server_one}'][@type='groupchat'][@to='{jid_two}/{resource_two}']/subject[not(text())]"),
+
+ # Moderator kicks participant
+ send_stanza("<iq id='kick1' to='#foo%{irc_server_one}' from='{jid_one}/{resource_one}' type='set'><query xmlns='http://jabber.org/protocol/muc#admin'><item nick='{nick_two}' role='none'><reason>reported</reason></item></query></iq>"),
+ expect_unordered(
+ [
+ "/presence[@type='unavailable'][@to='{jid_two}/{resource_one}']/muc_user:x/muc_user:item[@role='none']/muc_user:actor[@nick='{nick_one}']",
+ "/presence/muc_user:x/muc_user:item/muc_user:reason[text()='reported']",
+ "/presence/muc_user:x/muc_user:status[@code='307']",
+ "/presence/muc_user:x/muc_user:status[@code='110']"
+ ],
+ [
+ "/presence[@type='unavailable'][@to='{jid_two}/{resource_two}']/muc_user:x/muc_user:item[@role='none']/muc_user:actor[@nick='{nick_one}']",
+ "/presence/muc_user:x/muc_user:item/muc_user:reason[text()='reported']",
+ "/presence/muc_user:x/muc_user:status[@code='307']",
+ "/presence/muc_user:x/muc_user:status[@code='110']"
+ ],
+ ["/presence[@type='unavailable']/muc_user:x/muc_user:item[@role='none']/muc_user:actor[@nick='{nick_one}']",
+ "/presence/muc_user:x/muc_user:item/muc_user:reason[text()='reported']",
+ "/presence/muc_user:x/muc_user:status[@code='307']",
+ ],
+ [
+ "/iq[@id='kick1'][@type='result']"
+ ]
+ ),
+)
diff --git a/tests/end_to_end/scenarios/multisessionnick.py b/tests/end_to_end/scenarios/multisessionnick.py
new file mode 100644
index 0000000..4e72ce7
--- /dev/null
+++ b/tests/end_to_end/scenarios/multisessionnick.py
@@ -0,0 +1,125 @@
+from scenarios import *
+
+from scenarios.simple_channel_join import expect_self_join_presence
+
+scenario = (
+ # Resource one joins a channel
+ send_stanza("<presence from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}/{nick_one}' />"),
+ sequences.connection(),
+ expect_self_join_presence(jid = '{jid_one}/{resource_one}', chan = "#foo", nick = "{nick_one}"),
+
+ # The other resources joins the same room, with the same nick
+ send_stanza("<presence from='{jid_one}/{resource_two}' to='#foo%{irc_server_one}/{nick_one}' />"),
+
+ # We receive our own join
+ expect_unordered(
+ [
+ "/presence[@to='{jid_one}/{resource_two}'][@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'][@to='{jid_one}/{resource_two}']/subject[not(text())]"
+
+ ]
+ ),
+
+ # A different user joins the same room
+ send_stanza("<presence from='{jid_two}/{resource_one}' to='#foo%{irc_server_one}/{nick_two}' />"),
+ sequences.connection("irc.localhost", '{jid_two}/{resource_one}'),
+ expect_unordered(
+ # The new user’s presence is sent to the the existing occupant (two resources)
+ [
+ "/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}']"
+ ],
+ # the new user receives her own presence
+ [
+ "/presence[@to='{jid_two}/{resource_one}'][@from='#foo%{irc_server_one}/{nick_two}']",
+ "/presence/muc_user:x/muc_user:status[@code='110']"
+ ],
+ # the new user receives the presence of the existing occupant
+ [
+ "/presence[@to='{jid_two}/{resource_one}'][@from='#foo%{irc_server_one}/{nick_one}']",
+ ],
+ [
+ "/message[@from='#foo%{irc_server_one}'][@type='groupchat']/subject[not(text())]",
+ ],
+ ),
+
+ # That second user sends a private message to the first one
+ 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
+ expect_unordered(
+ [
+ "/message[@from='{lower_nick_two}%{irc_server_one}'][@to='{jid_one}/{resource_one}'][@type='chat']/body[text()='RELLO']",
+ "/message/hints:no-copy",
+ "/message/carbon:private",
+ "!/message/muc_user:x",
+ ],
+ [
+ "/message[@from='{lower_nick_two}%{irc_server_one}'][@to='{jid_one}/{resource_two}'][@type='chat']/body[text()='RELLO']",
+ "/message/hints:no-copy",
+ "/message/carbon:private",
+ "!/message/muc_user:x",
+ ]
+ ),
+
+ # First occupant (with the two resources) changes her/his nick to a conflicting one
+ send_stanza("<presence from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}/{nick_two}' />"),
+ expect_unordered(
+ ["/message[@to='{jid_one}/{resource_one}'][@type='chat']/body[text()='irc.localhost: Bobby: Nickname is already in use.']"],
+ ["/message[@to='{jid_one}/{resource_two}'][@type='chat']/body[text()='irc.localhost: Bobby: Nickname is already in use.']"],
+ ["/presence[@to='{jid_one}/{resource_one}'][@from='#foo%{irc_server_one}/{nick_two}'][@type='error']"],
+ ["/presence[@to='{jid_one}/{resource_two}'][@from='#foo%{irc_server_one}/{nick_two}'][@type='error']"]
+ ),
+
+ # First occupant (with the two resources) changes her/his nick
+ send_stanza("<presence from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}/{nick_three}' />"),
+ 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']"
+ ],
+ [
+ "/presence[@from='#foo%{irc_server_one}/{nick_one}'][@to='{jid_one}/{resource_two}'][@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_two}']",
+ "/presence/muc_user:x/muc_user:status[@code='110']"
+ ]
+ ),
+
+ # One resource leaves the server entirely.
+ send_stanza("<presence type='unavailable' from='{jid_one}/{resource_two}' to='#foo%{irc_server_one}/{nick_one}' />"),
+ # The leave is forwarded only to that resource
+ expect_stanza("/presence[@type='unavailable']/muc_user:x/muc_user:status[@code='110']",
+ "/presence/status[text()='Biboumi note: 1 resources are still in this channel.']",
+ ),
+
+ # The second user sends two new private messages to the first user
+ send_stanza("<message from='{jid_two}/{resource_one}' to='#foo%{irc_server_one}/{nick_three}' type='chat'><body>first</body></message>"),
+ send_stanza("<message from='{jid_two}/{resource_one}' to='#foo%{irc_server_one}/{nick_three}' type='chat'><body>second</body></message>"),
+
+ # The first user receives the two messages, on the connected resource, once each
+ expect_unordered(
+ ["/message[@from='{lower_nick_two}%{irc_server_one}'][@to='{jid_one}/{resource_one}'][@type='chat']/body[text()='first']"],
+ ["/message[@from='{lower_nick_two}%{irc_server_one}'][@to='{jid_one}/{resource_one}'][@type='chat']/body[text()='second']"]
+ ),
+)
diff --git a/tests/end_to_end/scenarios/nick_change_in_join.py b/tests/end_to_end/scenarios/nick_change_in_join.py
new file mode 100644
index 0000000..47aecc6
--- /dev/null
+++ b/tests/end_to_end/scenarios/nick_change_in_join.py
@@ -0,0 +1,18 @@
+from scenarios import *
+
+from scenarios.simple_channel_join import expect_self_join_presence
+
+scenario = (
+ send_stanza("<presence from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}/{nick_one}' />"),
+ sequences.connection(),
+ expect_self_join_presence(jid = '{jid_one}/{resource_one}', chan = "#foo", nick = "{nick_one}"),
+
+ send_stanza("<presence from='{jid_one}/{resource_one}' to='#bar%{irc_server_one}/{nick_two}' />"),
+ expect_stanza("/message/body[text()='Mode #bar [+nt] by {irc_host_one}']"),
+ expect_stanza("/presence[@to='{jid_one}/{resource_one}'][@from='#bar%{irc_server_one}/{nick_one}']/muc_user:x/muc_user:item[@affiliation='admin'][@role='moderator']",
+ "/presence/muc_user:x/muc_user:status[@code='110']",
+ "/presence/muc_user:x/muc_user:status[@code='210']", # This status signals that the server forced our nick to NOT be the one we asked
+ ),
+ expect_stanza("/message[@from='#bar%{irc_server_one}'][@type='groupchat']/subject[not(text())]"),
+)
+
diff --git a/tests/end_to_end/scenarios/not_connected_error.py b/tests/end_to_end/scenarios/not_connected_error.py
new file mode 100644
index 0000000..cc9fb35
--- /dev/null
+++ b/tests/end_to_end/scenarios/not_connected_error.py
@@ -0,0 +1,12 @@
+from scenarios import *
+
+from scenarios.simple_channel_join import expect_self_join_presence
+
+scenario = (
+ send_stanza("<presence type='unavailable' from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}/{nick_one}' />"),
+ # Fixme: what is the purpose of this test? Check that we don’t receive anything here…?
+
+ send_stanza("<presence from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}/{nick_one}' />"),
+ sequences.connection(),
+ expect_self_join_presence(jid='{jid_one}/{resource_one}', chan="#foo", nick="{nick_one}"),
+)
diff --git a/tests/end_to_end/scenarios/notices.py b/tests/end_to_end/scenarios/notices.py
new file mode 100644
index 0000000..dddee5d
--- /dev/null
+++ b/tests/end_to_end/scenarios/notices.py
@@ -0,0 +1,10 @@
+from scenarios import *
+
+import scenarios.simple_channel_join
+
+scenario = (
+ scenarios.simple_channel_join.scenario,
+
+ 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>"),
+ expect_stanza("/message[@from='#foo%{irc_server_one}/{nick_one}'][@type='groupchat']/body[text()='[notice] [#foo] Hello in a notice.']"),
+)
diff --git a/tests/end_to_end/scenarios/persistent_channel.py b/tests/end_to_end/scenarios/persistent_channel.py
new file mode 100644
index 0000000..3521a84
--- /dev/null
+++ b/tests/end_to_end/scenarios/persistent_channel.py
@@ -0,0 +1,48 @@
+from scenarios import *
+
+import scenarios.simple_channel_join
+
+scenario = (
+ # Join the channel with user 1
+ scenarios.simple_channel_join.scenario,
+
+ # Make it persistent for user 1
+ send_stanza("<iq from='{jid_one}/{resource_one}' id='conf1' to='#foo%{irc_server_one}' type='get'><query xmlns='http://jabber.org/protocol/muc#owner'/></iq>"),
+ expect_stanza("/iq[@type='result']/muc_owner:query/dataform:x/dataform:field[@var='persistent'][@type='boolean']/dataform:value[text()='false']"),
+ send_stanza("<iq from='{jid_one}/{resource_one}' id='conf2' to='#foo%{irc_server_one}' type='set'><query xmlns='http://jabber.org/protocol/muc#owner'><x type='submit' xmlns='jabber:x:data'><field var='persistent' xmlns='jabber:x:data'><value>true</value></field></x></query></iq>"),
+ expect_stanza("/iq[@type='result']"),
+
+ # Check that the value is now effectively true
+ send_stanza("<iq from='{jid_one}/{resource_one}' id='conf1' to='#foo%{irc_server_one}' type='get'><query xmlns='http://jabber.org/protocol/muc#owner'/></iq>"),
+ expect_stanza("/iq[@type='result']/muc_owner:query/dataform:x/dataform:field[@var='persistent'][@type='boolean']/dataform:value[text()='true']"),
+
+ # A second user joins the same channel
+ send_stanza("<presence from='{jid_two}/{resource_one}' to='#foo%{irc_server_one}/{nick_two}' />"),
+ sequences.connection("irc.localhost", '{jid_two}/{resource_one}'),
+ expect_unordered(
+ ["/presence[@to='{jid_one}/{resource_one}'][@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[@to='{jid_two}/{resource_one}'][@from='#foo%{irc_server_one}/{nick_one}']"],
+ ),
+ expect_stanza("/message[@from='#foo%{irc_server_one}'][@type='groupchat']/subject[not(text())]"),
+
+ # First user leaves the room (but biboumi will stay in the channel)
+ send_stanza("<presence from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}/{nick_one}' type='unavailable' />"),
+
+ # Only user 1 receives the unavailable presence
+ 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/muc_user:x/muc_user:item[@affiliation='admin'][@role='moderator']"),
+
+ # Second user sends a channel message
+ send_stanza("<message type='groupchat' from='{jid_two}/{resource_one}' to='#foo%{irc_server_one}'><body>coucou</body></message>"),
+
+ # Message should only be received by user 2, since user 1 has no resource in the room
+ expect_stanza("/message[@type='groupchat'][@to='{jid_two}/{resource_one}'][@from='#foo%{irc_server_one}/{nick_two}']"),
+
+ # Second user leaves the channel
+ send_stanza("<presence type='unavailable' from='{jid_two}/{resource_one}' to='#foo%{irc_server_one}/{nick_two}' />"),
+ expect_stanza("/presence[@type='unavailable'][@from='#foo%{irc_server_one}/{nick_two}']"),
+)
diff --git a/tests/end_to_end/scenarios/quit.py b/tests/end_to_end/scenarios/quit.py
new file mode 100644
index 0000000..ced5a96
--- /dev/null
+++ b/tests/end_to_end/scenarios/quit.py
@@ -0,0 +1,12 @@
+from scenarios import *
+
+import scenarios.simple_channel_join
+
+scenario = (
+ scenarios.simple_channel_join.scenario,
+
+ # Send a raw QUIT message
+ send_stanza("<message from='{jid_one}/{resource_one}' to='{irc_server_one}' type='chat'><body>QUIT bye bye</body></message>"),
+ expect_stanza("/presence[@from='#foo%{irc_server_one}/{nick_one}'][@type='unavailable']/muc_user:x/muc_user:status[@code='110']"),
+)
+
diff --git a/tests/end_to_end/scenarios/raw_message.py b/tests/end_to_end/scenarios/raw_message.py
new file mode 100644
index 0000000..96a3f3d
--- /dev/null
+++ b/tests/end_to_end/scenarios/raw_message.py
@@ -0,0 +1,12 @@
+from scenarios import *
+
+scenario = (
+ send_stanza("<presence from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}/{nick_one}' />"),
+ sequences.connection("irc.localhost", '{jid_one}/{resource_one}'),
+ expect_stanza("/message"),
+ expect_stanza("/presence"),
+ expect_stanza("/message"),
+
+ send_stanza("<message from='{jid_one}/{resource_one}' to='{irc_server_one}' type='chat'><body>WHOIS {nick_one}</body></message>"),
+ expect_stanza("/message[@from='{irc_server_one}'][@type='chat']/body[text()='irc.localhost: {nick_one} ~{nick_one} localhost * {nick_one}']"),
+)
diff --git a/tests/end_to_end/scenarios/raw_message_fixed_irc_server.py b/tests/end_to_end/scenarios/raw_message_fixed_irc_server.py
new file mode 100644
index 0000000..8196d12
--- /dev/null
+++ b/tests/end_to_end/scenarios/raw_message_fixed_irc_server.py
@@ -0,0 +1,15 @@
+from scenarios import *
+
+conf = 'fixed_server'
+
+scenario = (
+ send_stanza("<presence from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}/{nick_one}' />"),
+ sequences.connection("irc.localhost", '{jid_one}/{resource_one}', fixed_irc_server=True),
+ expect_stanza("/message"),
+ expect_stanza("/presence"),
+ expect_stanza("/message"),
+
+ send_stanza("<message from='{jid_one}/{resource_one}' to='{biboumi_host}' type='chat'><body>WHOIS {nick_one}</body></message>"),
+ expect_stanza("/message[@from='{biboumi_host}'][@type='chat']/body[text()='irc.localhost: {nick_one} ~{nick_one} localhost * {nick_one}']"),
+
+)
diff --git a/tests/end_to_end/scenarios/raw_names_command.py b/tests/end_to_end/scenarios/raw_names_command.py
new file mode 100644
index 0000000..09b47be
--- /dev/null
+++ b/tests/end_to_end/scenarios/raw_names_command.py
@@ -0,0 +1,13 @@
+from functions import send_stanza, expect_stanza
+
+import scenarios.simple_channel_join
+
+join_channel = scenarios.simple_channel_join.scenario
+
+scenario = (
+ join_channel,
+
+ send_stanza("<message type='chat' from='{jid_one}/{resource_one}' to='{irc_server_one}'><body>NAMES</body></message>"),
+ expect_stanza("/message/body[text()='irc.localhost: = #foo @{nick_one} ']"),
+ expect_stanza("/message/body[text()='irc.localhost: * End of /NAMES list. ']"),
+)
diff --git a/tests/end_to_end/scenarios/resource_is_removed_from_server_when_last_chan_is_left.py b/tests/end_to_end/scenarios/resource_is_removed_from_server_when_last_chan_is_left.py
new file mode 100644
index 0000000..8d208f0
--- /dev/null
+++ b/tests/end_to_end/scenarios/resource_is_removed_from_server_when_last_chan_is_left.py
@@ -0,0 +1,42 @@
+from scenarios import *
+
+scenario = (
+ # Join the channel
+ send_stanza("<presence from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}/{nick_one}' />"),
+ sequences.connection("irc.localhost", '{jid_one}/{resource_one}'),
+ expect_stanza("/message/body[text()='Mode #foo [+nt] by {irc_host_one}']"),
+ 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']"),
+ expect_stanza("/message[@from='#foo%{irc_server_one}'][@type='groupchat'][@to='{jid_one}/{resource_one}']/subject[not(text())]"),
+
+ # Make it persistent
+ send_stanza("<iq from='{jid_one}/{resource_one}' id='conf1' to='#foo%{irc_server_one}' type='get'><query xmlns='http://jabber.org/protocol/muc#owner'/></iq>"),
+ expect_stanza("/iq[@type='result']/muc_owner:query/dataform:x/dataform:field[@var='persistent'][@type='boolean']/dataform:value[text()='false']"),
+ send_stanza("<iq from='{jid_one}/{resource_one}' id='conf2' to='#foo%{irc_server_one}' type='set'><query xmlns='http://jabber.org/protocol/muc#owner'><x type='submit' xmlns='jabber:x:data'><field var='persistent' xmlns='jabber:x:data'><value>true</value></field></x></query></iq>"),
+ expect_stanza("/iq[@type='result']"),
+
+ send_stanza("<presence from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}/{nick_one}' type='unavailable' />"),
+ expect_stanza("/presence[@type='unavailable'][@from='#foo%{irc_server_one}/{nick_one}']"),
+
+ # Join the same channel, with the same JID, but a different resource
+ send_stanza("<presence from='{jid_one}/{resource_two}' to='#foo%{irc_server_one}/{nick_one}' />"),
+ expect_stanza("/presence[@to='{jid_one}/{resource_two}'][@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']"),
+ expect_stanza("/message[@from='#foo%{irc_server_one}'][@type='groupchat'][@to='{jid_one}/{resource_two}']/subject[not(text())]"),
+
+ # Join some other channel with someone else
+ send_stanza("<presence from='{jid_two}/{resource_one}' to='#bar%{irc_server_one}/{nick_two}' />"),
+ sequences.connection("irc.localhost", '{jid_two}/{resource_one}'),
+ expect_stanza("/message/body[text()='Mode #bar [+nt] by {irc_host_one}']"),
+ expect_stanza("/presence[@to='{jid_two}/{resource_one}'][@from='#bar%{irc_server_one}/{nick_two}']/muc_user:x/muc_user:item[@affiliation='admin'][@role='moderator']",
+ "/presence/muc_user:x/muc_user:status[@code='110']"),
+ expect_stanza("/message[@from='#bar%{irc_server_one}'][@type='groupchat'][@to='{jid_two}/{resource_one}']/subject[not(text())]"),
+
+ # Send two messages from the second user to the first one
+ send_stanza("<message from='{jid_two}/{resource_one}' to='{lower_nick_one}%{irc_server_one}' type='chat'><body>kikoo</body></message>"),
+ send_stanza("<message from='{jid_two}/{resource_one}' to='{lower_nick_one}%{irc_server_one}' type='chat'><body>second kikoo</body></message>"),
+
+ # We must receive each message only once, no duplicate
+ expect_stanza("/message/body[text()='kikoo']"),
+ expect_stanza("/message/body[text()='second kikoo']"),
+)
diff --git a/tests/end_to_end/scenarios/self_disco_info.py b/tests/end_to_end/scenarios/self_disco_info.py
new file mode 100644
index 0000000..6430dbd
--- /dev/null
+++ b/tests/end_to_end/scenarios/self_disco_info.py
@@ -0,0 +1,11 @@
+from scenarios import *
+
+scenario = (
+ send_stanza("<iq type='get' id='get1' from='{jid_one}/{resource_one}' to='{biboumi_host}'><query xmlns='http://jabber.org/protocol/disco#info'/></iq>"),
+ expect_stanza("/iq[@type='result']/disco_info:query/disco_info:identity[@category='conference'][@type='irc'][@name='Biboumi XMPP-IRC gateway']",
+ "/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']"),
+)
diff --git a/tests/end_to_end/scenarios/self_invite.py b/tests/end_to_end/scenarios/self_invite.py
new file mode 100644
index 0000000..7959b3a
--- /dev/null
+++ b/tests/end_to_end/scenarios/self_invite.py
@@ -0,0 +1,7 @@
+from scenarios import *
+
+scenario = (
+ scenarios.simple_channel_join.scenario,
+ send_stanza("<message from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}'><x xmlns='http://jabber.org/protocol/muc#user'><invite to='{nick_one}'/></x></message>"),
+ expect_stanza("/message/body[text()='{nick_one} is already on channel #foo']")
+)
diff --git a/tests/end_to_end/scenarios/self_ping_fixed_server.py b/tests/end_to_end/scenarios/self_ping_fixed_server.py
new file mode 100644
index 0000000..453387c
--- /dev/null
+++ b/tests/end_to_end/scenarios/self_ping_fixed_server.py
@@ -0,0 +1,11 @@
+from scenarios import *
+
+conf = "fixed_server"
+
+scenario = (
+ scenarios.simple_channel_join_fixed.scenario,
+
+ # Send a ping to ourself
+ send_stanza("<iq type='get' from='{jid_one}/{resource_one}' id='first_ping' to='#foo@{biboumi_host}/{nick_one}'><ping xmlns='urn:xmpp:ping' /></iq>"),
+ expect_stanza("/iq[@from='#foo@{biboumi_host}/{nick_one}'][@type='result'][@to='{jid_one}/{resource_one}'][@id='first_ping']"),
+)
diff --git a/tests/end_to_end/scenarios/self_ping_not_in_muc.py b/tests/end_to_end/scenarios/self_ping_not_in_muc.py
new file mode 100644
index 0000000..eb7d092
--- /dev/null
+++ b/tests/end_to_end/scenarios/self_ping_not_in_muc.py
@@ -0,0 +1,15 @@
+from scenarios import *
+
+scenario = (
+ scenarios.simple_channel_join.scenario,
+
+ # Send a ping to ourself, in a muc where we’re not
+ send_stanza("<iq type='get' from='{jid_one}/{resource_one}' id='first_ping' to='#nil%{irc_server_one}/{nick_one}'><ping xmlns='urn:xmpp:ping' /></iq>"),
+ # Immediately receive an error
+ expect_stanza("/iq[@from='#nil%{irc_server_one}/{nick_one}'][@type='error'][@to='{jid_one}/{resource_one}'][@id='first_ping']/error/stanza:not-acceptable"),
+
+ # Send a ping to ourself, in a muc where we are, but not this resource
+ send_stanza("<iq type='get' from='{jid_one}/{resource_two}' id='first_ping' to='#foo%{irc_server_one}/{nick_one}'><ping xmlns='urn:xmpp:ping' /></iq>"),
+ # Immediately receive an error
+ expect_stanza("/iq[@from='#foo%{irc_server_one}/{nick_one}'][@type='error'][@to='{jid_one}/{resource_two}'][@id='first_ping']/error/stanza:not-acceptable"),
+)
diff --git a/tests/end_to_end/scenarios/self_ping_on_real_channel.py b/tests/end_to_end/scenarios/self_ping_on_real_channel.py
new file mode 100644
index 0000000..6cbb210
--- /dev/null
+++ b/tests/end_to_end/scenarios/self_ping_on_real_channel.py
@@ -0,0 +1,23 @@
+from scenarios import *
+
+scenario = (
+ scenarios.simple_channel_join.scenario,
+
+ # Send a ping to ourself
+ send_stanza("<iq type='get' from='{jid_one}/{resource_one}' id='first_ping' to='#foo%{irc_server_one}/{nick_one}'><ping xmlns='urn:xmpp:ping' /></iq>"),
+ expect_stanza("/iq[@from='#foo%{irc_server_one}/{nick_one}'][@type='result'][@to='{jid_one}/{resource_one}'][@id='first_ping']"),
+
+ # Now join the same room, from the same bare JID, behind the same nick
+ send_stanza("<presence from='{jid_one}/{resource_two}' to='#foo%{irc_server_one}/{nick_one}' />"),
+ expect_stanza("/presence[@to='{jid_one}/{resource_two}'][@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']"),
+
+ expect_stanza("/message[@from='#foo%{irc_server_one}'][@type='groupchat'][@to='{jid_one}/{resource_two}']/subject[not(text())]"),
+
+ # And re-send a self ping
+ send_stanza("<iq type='get' from='{jid_one}/{resource_one}' id='second_ping' to='#foo%{irc_server_one}/{nick_one}'><ping xmlns='urn:xmpp:ping' /></iq>"),
+ expect_stanza("/iq[@from='#foo%{irc_server_one}/{nick_one}'][@type='result'][@to='{jid_one}/{resource_one}'][@id='second_ping']"),
+ ## And re-do exactly the same thing, just change the resource initiating the self ping
+ send_stanza("<iq type='get' from='{jid_one}/{resource_two}' id='third_ping' to='#foo%{irc_server_one}/{nick_one}'><ping xmlns='urn:xmpp:ping' /></iq>"),
+ expect_stanza("/iq[@from='#foo%{irc_server_one}/{nick_one}'][@type='result'][@to='{jid_one}/{resource_two}'][@id='third_ping']"),
+)
diff --git a/tests/end_to_end/scenarios/self_ping_with_error.py b/tests/end_to_end/scenarios/self_ping_with_error.py
new file mode 100644
index 0000000..0266d20
--- /dev/null
+++ b/tests/end_to_end/scenarios/self_ping_with_error.py
@@ -0,0 +1,13 @@
+from scenarios import *
+
+scenario = (
+ scenarios.simple_channel_join.scenario,
+
+ # Send a ping to ourself
+ send_stanza("<iq type='get' from='{jid_one}/{resource_one}' id='first_ping' to='#foo%{irc_server_one}/{nick_one}'><ping xmlns='urn:xmpp:ping' /></iq>"),
+ expect_stanza("/iq[@from='#foo%{irc_server_one}/{nick_one}'][@type='result'][@to='{jid_one}/{resource_one}'][@id='first_ping']"),
+
+ # Send a ping to ourself
+ send_stanza("<iq type='get' from='{jid_one}/{resource_one}' id='first_ping' to='#foo%{irc_server_one}/{nick_one}'><ping xmlns='urn:xmpp:ping' /></iq>"),
+ expect_stanza("/iq[@from='#foo%{irc_server_one}/{nick_one}'][@type='result'][@to='{jid_one}/{resource_one}'][@id='first_ping']"),
+)
diff --git a/tests/end_to_end/scenarios/self_version.py b/tests/end_to_end/scenarios/self_version.py
new file mode 100644
index 0000000..f567355
--- /dev/null
+++ b/tests/end_to_end/scenarios/self_version.py
@@ -0,0 +1,39 @@
+from scenarios import *
+
+scenario = (
+ scenarios.simple_channel_join.scenario,
+
+ # Send a version request to ourself
+ send_stanza("<iq type='get' from='{jid_one}/{resource_one}' id='first_version' to='#foo%{irc_server_one}/{nick_one}'><query xmlns='jabber:iq:version' /></iq>"),
+ # We receive our own request,
+ expect_stanza("/iq[@from='{lower_nick_one}%{irc_server_one}'][@type='get'][@to='{jid_one}/{resource_one}']",
+ after = save_value("id", extract_attribute("/iq", 'id'))),
+ # Respond to the request, and receive our own response
+ send_stanza("<iq type='result' to='{lower_nick_one}%{irc_server_one}' id='{id}' from='{jid_one}/{resource_one}'><query xmlns='jabber:iq:version'><name>e2e test</name><version>1.0</version><os>Fedora</os></query></iq>"),
+ expect_stanza("/iq[@from='#foo%{irc_server_one}/{nick_one}'][@type='result'][@to='{jid_one}/{resource_one}'][@id='first_version']/version:query/version:name[text()='e2e test (through the biboumi gateway) 1.0 Fedora']"),
+
+ # Now join the same room, from the same bare JID, behind the same nick
+ send_stanza("<presence from='{jid_one}/{resource_two}' to='#foo%{irc_server_one}/{nick_one}' />"),
+ expect_stanza("/presence[@to='{jid_one}/{resource_two}'][@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']"),
+ expect_stanza("/message[@from='#foo%{irc_server_one}'][@type='groupchat'][@to='{jid_one}/{resource_two}']/subject[not(text())]"),
+
+ # And re-send a self ping
+ send_stanza("<iq type='get' from='{jid_one}/{resource_two}' id='second_version' to='#foo%{irc_server_one}/{nick_one}'><query xmlns='jabber:iq:version' /></iq>"),
+ # We receive our own request. Note that we don't know the `to` value, it could be one of our two resources.
+ expect_stanza("/iq[@from='{lower_nick_one}%{irc_server_one}'][@type='get'][@to]",
+ after = (save_value("to", extract_attribute("/iq", "to")),
+ save_value("id", extract_attribute("/iq", "id")))),
+ # Respond to the request, using the extracted 'to' value as our 'from'
+ send_stanza("<iq type='result' to='{lower_nick_one}%{irc_server_one}' id='{id}' from='{to}'><query xmlns='jabber:iq:version'><name>e2e test</name><version>1.0</version><os>Fedora</os></query></iq>"),
+ expect_stanza("/iq[@from='#foo%{irc_server_one}/{nick_one}'][@type='result'][@to='{jid_one}/{resource_two}'][@id='second_version']"),
+
+ # And do exactly the same thing, but initiated by the other resource
+ send_stanza("<iq type='get' from='{jid_one}/{resource_one}' id='second_version' to='#foo%{irc_server_one}/{nick_one}'><query xmlns='jabber:iq:version' /></iq>"),
+ expect_stanza("/iq[@from='{lower_nick_one}%{irc_server_one}'][@type='get'][@to]",
+ after = (save_value("to", extract_attribute("/iq", "to")),
+ save_value("id", extract_attribute("/iq", "id")))),
+ # Respond to the request, using the extracted 'to' value as our 'from'
+ send_stanza("<iq type='result' to='{lower_nick_one}%{irc_server_one}' id='{id}' from='{to}'><query xmlns='jabber:iq:version'><name>e2e test</name><version>1.0</version><os>Fedora</os></query></iq>"),
+ expect_stanza("/iq[@from='#foo%{irc_server_one}/{nick_one}'][@type='result'][@to='{jid_one}/{resource_one}'][@id='second_version']"),
+)
diff --git a/tests/end_to_end/scenarios/simple_channel_join.py b/tests/end_to_end/scenarios/simple_channel_join.py
new file mode 100644
index 0000000..b09d6be
--- /dev/null
+++ b/tests/end_to_end/scenarios/simple_channel_join.py
@@ -0,0 +1,21 @@
+from scenarios import *
+
+def expect_self_join_presence(jid, chan, nick, irc_server="{irc_server_one}"):
+ return (
+ expect_stanza("/message/body[text()='Mode " + chan + " [+nt] by irc.localhost']"),
+ expect_stanza("/presence[@to='" + jid +"'][@from='" + chan + "%" + irc_server + "/" + nick + "']/muc_user:x/muc_user:item[@affiliation='admin'][@role='moderator']",
+ "/presence/muc_user:x/muc_user:status[@code='100']", # Rooms are all non-anonymous
+ "/presence/muc_user:x/muc_user:status[@code='110']",
+ ),
+ expect_stanza("/message[@from='" + chan + "%" + irc_server + "'][@type='groupchat']/subject[not(text())]"),
+ )
+
+
+scenario = (
+ send_stanza("<presence from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}/{nick_one}' />"),
+ sequences.connection(),
+
+ expect_self_join_presence(jid = '{jid_one}/{resource_one}', chan = "#foo", nick = "{nick_one}"),
+
+)
+
diff --git a/tests/end_to_end/scenarios/simple_channel_join_fixed.py b/tests/end_to_end/scenarios/simple_channel_join_fixed.py
new file mode 100644
index 0000000..6efd20f
--- /dev/null
+++ b/tests/end_to_end/scenarios/simple_channel_join_fixed.py
@@ -0,0 +1,12 @@
+from scenarios import *
+
+conf = "fixed_server"
+
+scenario = (
+ send_stanza("<presence from='{jid_one}/{resource_one}' to='#foo@{biboumi_host}/{nick_one}' />"),
+ sequences.connection("irc.localhost", '{jid_one}/{resource_one}', fixed_irc_server=True),
+ expect_stanza("/message/body[text()='Mode #foo [+nt] by {irc_host_one}']"),
+ expect_stanza("/presence[@to='{jid_one}/{resource_one}'][@from='#foo@{biboumi_host}/{nick_one}']/muc_user:x/muc_user:item[@affiliation='admin'][@role='moderator']",
+ "/presence/muc_user:x/muc_user:status[@code='110']"),
+ expect_stanza("/message[@from='#foo@{biboumi_host}'][@type='groupchat']/subject[not(text())]"),
+)
diff --git a/tests/end_to_end/scenarios/simple_channel_list.py b/tests/end_to_end/scenarios/simple_channel_list.py
new file mode 100644
index 0000000..7406e86
--- /dev/null
+++ b/tests/end_to_end/scenarios/simple_channel_list.py
@@ -0,0 +1,14 @@
+from scenarios import *
+
+scenario = (
+ scenarios.multiple_channels_join.scenario,
+
+ send_stanza("<iq from='{jid_one}/{resource_one}' id='id1' to='{irc_server_one}' type='get'><query xmlns='http://jabber.org/protocol/disco#items'/></iq>"),
+ expect_stanza("/iq[@type='result']/disco_items:query",
+ "/iq/disco_items:query/rsm:set/rsm:count[text()='3']",
+ "/iq/disco_items:query/rsm:set/rsm:first",
+ "/iq/disco_items:query/rsm:set/rsm:last",
+ "/iq/disco_items:query/disco_items:item[@jid='#foo%{irc_server_one}']",
+ "/iq/disco_items:query/disco_items:item[@jid='#bar%{irc_server_one}']",
+ "/iq/disco_items:query/disco_items:item[@jid='#baz%{irc_server_one}']"),
+)
diff --git a/tests/end_to_end/scenarios/simple_kick.py b/tests/end_to_end/scenarios/simple_kick.py
new file mode 100644
index 0000000..0e06589
--- /dev/null
+++ b/tests/end_to_end/scenarios/simple_kick.py
@@ -0,0 +1,49 @@
+from scenarios import *
+
+scenario = (
+ scenarios.channel_join_with_two_users.scenario,
+ # demonstrate bug https://lab.louiz.org/louiz/biboumi/issues/3291
+ # First user joins an other channel
+ send_stanza("<presence from='{jid_one}/{resource_one}' to='#bar%{irc_server_one}/{nick_one}' />"),
+ expect_stanza("/message"),
+ expect_stanza("/presence/muc_user:x/muc_user:status[@code='110']"),
+ expect_stanza("/message[@type='groupchat']/subject"),
+
+ # Second user joins
+ send_stanza("<presence from='{jid_two}/{resource_one}' to='#bar%{irc_server_one}/{nick_two}' />"),
+ expect_unordered(
+ ["/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"]
+ ),
+
+ # Moderator kicks participant
+ send_stanza("<iq id='kick1' to='#foo%{irc_server_one}' from='{jid_one}/{resource_one}' type='set'><query xmlns='http://jabber.org/protocol/muc#admin'><item nick='{nick_two}' role='none'><reason>reported</reason></item></query></iq>"),
+ expect_unordered(
+ [
+ "/presence[@type='unavailable'][@to='{jid_two}/{resource_one}']/muc_user:x/muc_user:item[@role='none']/muc_user:actor[@nick='{nick_one}']",
+ "/presence/muc_user:x/muc_user:item/muc_user:reason[text()='reported']",
+ "/presence/muc_user:x/muc_user:status[@code='307']",
+ "/presence/muc_user:x/muc_user:status[@code='110']"
+ ],
+ [
+ "/presence[@type='unavailable'][@to='{jid_one}/{resource_one}']/muc_user:x/muc_user:item[@role='none']/muc_user:actor[@nick='{nick_one}']",
+ "/presence/muc_user:x/muc_user:item/muc_user:reason[text()='reported']",
+ "/presence/muc_user:x/muc_user:status[@code='307']",
+ ],
+ ["/iq[@id='kick1'][@type='result']"]
+ ),
+
+ # Bug 3291, suite. We must not receive any presence from #foo, here
+ send_stanza("<message from='{jid_two}/{resource_one}' to='{irc_server_one}' type='chat'><body>QUIT bye bye</body></message>"),
+ expect_unordered(
+ ["/presence[@from='#bar%{irc_server_one}/{nick_two}'][@to='{jid_one}/{resource_one}']"],
+ ["/presence[@from='#bar%{irc_server_one}/{nick_two}'][@to='{jid_two}/{resource_one}']"],
+ ["/message"],
+ ["/message"],
+ ),
+)
diff --git a/tests/end_to_end/scenarios/simple_mam.py b/tests/end_to_end/scenarios/simple_mam.py
new file mode 100644
index 0000000..4509eeb
--- /dev/null
+++ b/tests/end_to_end/scenarios/simple_mam.py
@@ -0,0 +1,60 @@
+from scenarios import *
+
+scenario = (
+ scenarios.simple_channel_join.scenario,
+
+ # Send two channel messages
+ send_stanza("<message from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}' type='groupchat'><body>coucou</body></message>"),
+ 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_stanza("<message from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}' type='groupchat'><body>coucou 2</body></message>"),
+ expect_stanza("/message[@from='#foo%{irc_server_one}/{nick_one}'][@to='{jid_one}/{resource_one}'][@type='groupchat']/body[text()='coucou 2']"),
+
+ # Retrieve the complete archive
+ 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>"),
+
+ expect_stanza("/message/mam:result[@queryid='qid1']/forward:forwarded/delay:delay",
+ "/message/mam:result[@queryid='qid1']/forward:forwarded/client:message[@from='#foo%{irc_server_one}/{nick_one}'][@type='groupchat']/client:body[text()='coucou']"),
+ expect_stanza("/message/mam:result[@queryid='qid1']/forward:forwarded/delay:delay",
+ "/message/mam:result[@queryid='qid1']/forward:forwarded/client:message[@from='#foo%{irc_server_one}/{nick_one}'][@type='groupchat']/client:body[text()='coucou 2']"),
+
+ expect_stanza("/iq[@type='result'][@id='id1'][@from='#foo%{irc_server_one}'][@to='{jid_one}/{resource_one}']",
+ "/iq/mam:fin/rms:set/rsm:last",
+ "/iq/mam:fin/rsm:set/rsm:first",
+ "/iq/mam:fin[@complete='true']"),
+
+ # Retrieve an empty archive by specifying an early “end” date
+ 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'>
+ <x xmlns='jabber:x:data' type='submit'>
+ <field var='FORM_TYPE' type='hidden'> <value>urn:xmpp:mam:2</value></field>
+ <field var='end'><value>2000-06-07T00:00:00Z</value></field>
+ </x>
+ </query></iq>"""),
+
+ expect_stanza("/iq[@type='result'][@id='id2'][@from='#foo%{irc_server_one}'][@to='{jid_one}/{resource_one}']",
+ "/iq/mam:fin[@complete='true']/rsm:set"),
+
+ # Retrieve an empty archive by specifying a late “start” date
+ # (note that this test will break in ~1000 years)
+ 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'>
+ <x xmlns='jabber:x:data' type='submit'>
+ <field var='FORM_TYPE' type='hidden'> <value>urn:xmpp:mam:2</value></field>
+ <field var='start'><value>3016-06-07T00:00:00Z</value></field>
+ </x>
+ </query></iq>"""),
+
+ expect_stanza("/iq[@type='result'][@id='id3'][@from='#foo%{irc_server_one}'][@to='{jid_one}/{resource_one}']",
+ "/iq/mam:fin[@complete='true']/rsm:set"),
+
+ # Retrieve the whole archive, but limit the response to one elemet
+ 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>"),
+
+ 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']"),
+
+ expect_stanza("/iq[@type='result'][@id='id4'][@from='#foo%{irc_server_one}'][@to='{jid_one}/{resource_one}']",
+ "!/iq/mam:fin[@complete='true']/rsm:set"),
+)
diff --git a/tests/end_to_end/scenarios/slash_me_channel_message.py b/tests/end_to_end/scenarios/slash_me_channel_message.py
new file mode 100644
index 0000000..d30fba3
--- /dev/null
+++ b/tests/end_to_end/scenarios/slash_me_channel_message.py
@@ -0,0 +1,18 @@
+from scenarios import *
+
+scenario = (
+ scenarios.channel_join_with_two_users.scenario,
+ # Send a channel message
+ send_stanza("<message from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}' type='groupchat'><body>/me rit en IRC</body></message>"),
+ # Receive the message, forwarded to the two users
+ expect_unordered(
+ [
+ "/message[@from='#foo%{irc_server_one}/{nick_one}'][@to='{jid_one}/{resource_one}'][@type='groupchat']/body[text()='/me rit en IRC']",
+ "/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()='/me rit en IRC']",
+ "/message/stable_id:stanza-id[@by='#foo%{irc_server_one}'][@id]"
+ ],
+ ),
+)
diff --git a/tests/end_to_end/sequences.py b/tests/end_to_end/sequences.py
new file mode 100644
index 0000000..8a40e52
--- /dev/null
+++ b/tests/end_to_end/sequences.py
@@ -0,0 +1,108 @@
+from functions import expect_stanza, send_stanza, common_replacements
+
+def handshake():
+ return (
+ expect_stanza("//handshake"),
+ send_stanza("<handshake xmlns='jabber:component:accept'/>")
+ )
+
+def connection_begin(irc_host, jid, expected_irc_presence=False, fixed_irc_server=False):
+ jid = jid.format_map(common_replacements)
+ if fixed_irc_server:
+ xpath = "/message[@to='" + jid + "'][@from='biboumi.localhost']/body[text()='%s']"
+ xpath_re = "/message[@to='" + jid + "'][@from='biboumi.localhost']/body[re:test(text(), '%s')]"
+ else:
+ xpath = "/message[@to='" + jid + "'][@from='" + irc_host + "@biboumi.localhost']/body[text()='%s']"
+ xpath_re = "/message[@to='" + jid + "'][@from='" + irc_host + "@biboumi.localhost']/body[re:test(text(), '%s')]"
+ result = (
+ expect_stanza(xpath % ('Connecting to %s:6697 (encrypted)' % irc_host),
+ "/message/hints:no-copy",
+ "/message/carbon:private"
+ ),
+ expect_stanza(xpath % 'Connection failed: Connection refused'),
+ expect_stanza(xpath % ('Connecting to %s:6670 (encrypted)' % irc_host)),
+ expect_stanza(xpath % 'Connection failed: Connection refused'),
+ expect_stanza(xpath % ('Connecting to %s:6667 (not encrypted)' % irc_host)),
+ expect_stanza(xpath % 'Connected to IRC server.'))
+
+ if expected_irc_presence:
+ result += (expect_stanza("/presence[@from='" + irc_host + "@biboumi.localhost']"),)
+
+ # These five messages can be receive in any order
+ result += (
+ expect_stanza(xpath_re % (r'^%s: (\*\*\* Checking Ident|\*\*\* Looking up your hostname\.\.\.|\*\*\* Found your hostname: .*|ACK multi-prefix|\*\*\* Got Ident response)$' % 'irc.localhost')),
+ expect_stanza(xpath_re % (r'^%s: (\*\*\* Checking Ident|\*\*\* Looking up your hostname\.\.\.|\*\*\* Found your hostname: .*|ACK multi-prefix|\*\*\* Got Ident response)$' % 'irc.localhost')),
+ expect_stanza(xpath_re % (r'^%s: (\*\*\* Checking Ident|\*\*\* Looking up your hostname\.\.\.|\*\*\* Found your hostname: .*|ACK multi-prefix|\*\*\* Got Ident response)$' % 'irc.localhost')),
+ expect_stanza(xpath_re % (r'^%s: (\*\*\* Checking Ident|\*\*\* Looking up your hostname\.\.\.|\*\*\* Found your hostname: .*|ACK multi-prefix|\*\*\* Got Ident response)$' % 'irc.localhost')),
+ expect_stanza(xpath_re % (r'^%s: (\*\*\* Checking Ident|\*\*\* Looking up your hostname\.\.\.|\*\*\* Found your hostname: .*|ACK multi-prefix|\*\*\* Got Ident response)$' % 'irc.localhost')),
+ )
+
+ return result
+
+def connection_tls_begin(irc_host, jid, fixed_irc_server):
+ jid = jid.format_map(common_replacements)
+ if fixed_irc_server:
+ xpath = "/message[@to='" + jid + "'][@from='biboumi.localhost']/body[text()='%s']"
+ xpath_re = "/message[@to='" + jid + "'][@from='biboumi.localhost']/body[re:test(text(), '%s')]"
+ else:
+ xpath = "/message[@to='" + jid + "'][@from='" + irc_host + "@biboumi.localhost']/body[text()='%s']"
+ xpath_re = "/message[@to='" + jid + "'][@from='" + irc_host + "@biboumi.localhost']/body[re:test(text(), '%s')]"
+ irc_host = 'irc.localhost'
+ return (
+ expect_stanza(xpath % ('Connecting to %s:7778 (encrypted)' % irc_host),
+ "/message/hints:no-copy",
+ "/message/carbon:private",
+ ),
+ expect_stanza(xpath % 'Connected to IRC server (encrypted).'),
+ # These five messages can be receive in any order
+ expect_stanza(xpath_re % (r'^%s: (\*\*\* Checking Ident|\*\*\* Looking up your hostname\.\.\.|\*\*\* Found your hostname: .*|ACK multi-prefix|\*\*\* Got Ident response)$' % irc_host)),
+ expect_stanza(xpath_re % (r'^%s: (\*\*\* Checking Ident|\*\*\* Looking up your hostname\.\.\.|\*\*\* Found your hostname: .*|ACK multi-prefix|\*\*\* Got Ident response)$' % irc_host)),
+ expect_stanza(xpath_re % (r'^%s: (\*\*\* Checking Ident|\*\*\* Looking up your hostname\.\.\.|\*\*\* Found your hostname: .*|ACK multi-prefix|\*\*\* Got Ident response)$' % irc_host)),
+ expect_stanza(xpath_re % (r'^%s: (\*\*\* Checking Ident|\*\*\* Looking up your hostname\.\.\.|\*\*\* Found your hostname: .*|ACK multi-prefix|\*\*\* Got Ident response)$' % irc_host)),
+ expect_stanza(xpath_re % (r'^%s: (\*\*\* Checking Ident|\*\*\* Looking up your hostname\.\.\.|\*\*\* Found your hostname: .*|ACK multi-prefix|\*\*\* Got Ident response)$' % irc_host)),
+ )
+
+def connection_end(irc_host, jid, fixed_irc_server=False):
+ jid = jid.format_map(common_replacements)
+ if fixed_irc_server:
+ xpath = "/message[@to='" + jid + "'][@from='biboumi.localhost']/body[text()='%s']"
+ xpath_re = "/message[@to='" + jid + "'][@from='biboumi.localhost']/body[re:test(text(), '%s')]"
+ else:
+ xpath = "/message[@to='" + jid + "'][@from='" + irc_host + "@biboumi.localhost']/body[text()='%s']"
+ xpath_re = "/message[@to='" + jid + "'][@from='" + irc_host + "@biboumi.localhost']/body[re:test(text(), '%s')]"
+ irc_host = 'irc.localhost'
+ return (
+ expect_stanza(xpath_re % (r'^%s: Your host is .*$' % irc_host)),
+ expect_stanza(xpath_re % (r'^%s: This server was created .*$' % irc_host)),
+ expect_stanza(xpath_re % (r'^%s: There are \d+ users and \d+ invisible on \d+ servers$' % irc_host)),
+ expect_stanza(xpath_re % (r'^%s: \d+ unknown connection\(s\)$' % irc_host), optional=True),
+ expect_stanza(xpath_re % (r'^%s: \d+ channels formed$' % irc_host), optional=True),
+ expect_stanza(xpath_re % (r'^%s: I have \d+ clients and \d+ servers$' % irc_host)),
+ expect_stanza(xpath_re % (r'^%s: \d+ \d+ Current local users \d+, max \d+$' % irc_host)),
+ expect_stanza(xpath_re % (r'^%s: \d+ \d+ Current global users \d+, max \d+$' % irc_host)),
+ expect_stanza(xpath_re % (r'^%s: Highest connection count: \d+ \(\d+ clients\) \(\d+ connections received\)$' % irc_host)),
+ expect_stanza(xpath % "- This is charybdis MOTD you might replace it, but if not your friends will\n- laugh at you.\n"),
+ expect_stanza(xpath_re % r'^User mode for \w+ is \[\+Z?i\]$'),
+ )
+
+def connection_middle(irc_host, jid, fixed_irc_server=False):
+ if fixed_irc_server:
+ xpath_re = "/message[@to='" + jid + "'][@from='biboumi.localhost']/body[re:test(text(), '%s')]"
+ else:
+ xpath_re = "/message[@to='" + jid + "'][@from='" + irc_host + "@biboumi.localhost']/body[re:test(text(), '%s')]"
+ irc_host = 'irc.localhost'
+ return (
+ expect_stanza(xpath_re % (r'^%s: \*\*\* You are exempt from flood limits$' % irc_host)),
+ )
+
+
+def connection(irc_host="irc.localhost", jid="{jid_one}/{resource_one}", expected_irc_presence=False, fixed_irc_server=False):
+ return connection_begin(irc_host, jid, expected_irc_presence, fixed_irc_server=fixed_irc_server) + \
+ connection_middle(irc_host, jid, fixed_irc_server=fixed_irc_server) + \
+ connection_end(irc_host, jid, fixed_irc_server=fixed_irc_server)
+
+def connection_tls(irc_host="irc.localhost", jid="{jid_one}/{resource_one}", fixed_irc_server=False):
+ return connection_tls_begin(irc_host, jid, fixed_irc_server) + \
+ connection_middle(irc_host, jid, fixed_irc_server) +\
+ connection_end(irc_host, jid, fixed_irc_server)
+
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