diff options
author | Jonas Smedegaard <dr@jones.dk> | 2016-12-21 21:25:17 +0100 |
---|---|---|
committer | Jonas Smedegaard <dr@jones.dk> | 2016-12-21 21:25:17 +0100 |
commit | dc1ad45aea9a10007a31ae449b1e0fe352a8a6cd (patch) | |
tree | eb233ce97fc828e2edba69a82a1e7314b92771d3 | |
parent | a077b304b27d21bf635ef4f31a2a0447d3b14d11 (diff) | |
parent | f820d86aadb7a5473bcc0a0a3669732ab0182555 (diff) | |
download | biboumi-dc1ad45aea9a10007a31ae449b1e0fe352a8a6cd.tar.gz biboumi-dc1ad45aea9a10007a31ae449b1e0fe352a8a6cd.tar.bz2 biboumi-dc1ad45aea9a10007a31ae449b1e0fe352a8a6cd.tar.xz biboumi-dc1ad45aea9a10007a31ae449b1e0fe352a8a6cd.zip |
Merge tag 'upstream/4.0'
Upstream version 4.0
# gpg: Signature made Wed Dec 21 21:25:11 2016 CET
# gpg: using RSA key 2C7C3146C1A00121
# gpg: Good signature from "Jonas Smedegaard <dr@jones.dk>"
# gpg: aka "Jonas Smedegaard <jonas@homebase.dk>"
# gpg: aka "Jonas Smedegaard <js@debian.org>"
# gpg: aka "[jpeg image of size 4165]"
71 files changed, 2935 insertions, 1103 deletions
diff --git a/.gitignore b/.gitignore deleted file mode 100644 index 83cd31d..0000000 --- a/.gitignore +++ /dev/null @@ -1,40 +0,0 @@ -# Compiled Object files -*.slo -*.lo -*.o -*.obj - -# Precompiled Headers -*.gch -*.pch - -# Compiled Dynamic libraries -*.so -*.dylib -*.dll - -# Compiled Static libraries -*.lai -*.la -*.a -*.lib - -# Executables -*.exe -*.out -*.app - -# Gcov files -*.gcno -*.gcov -*.gcda - -# Python files -*.pyc -*.pyo - -# Build directories -build/ - -# Clion directory -.idea/
\ No newline at end of file diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml deleted file mode 100644 index c1cc979..0000000 --- a/.gitlab-ci.yml +++ /dev/null @@ -1,103 +0,0 @@ -before_script: - - uname -a - - whoami - - echo $LANG - - g++ --version - - clang++ --version - - rm -rf build/ - - mkdir build/ - - cd build - -variables: - COMPILER: "g++" - BUILD_TYPE: "Debug" - BOTAN: "-DWITH_BOTAN=1" - CARES: "-DWITH_CARES=1" - SYSTEMD: "-DWITH_SYSTEMD=1" - LIBIDN: "-DWITH_LIBIDN=1" - LITESQL: "-DWITH_LITESQL=1" - -.template:basic_build: &basic_build - stage: build - script: - - "echo Running cmake with the following parameters: -DCMAKE_CXX_COMPILER=${COMPILER} -DCMAKE_BUILD_TYPE=${BUILD_TYPE} ${BOTAN} ${CARES} ${SYSTEMD} ${LIBIDN} ${LITESQL}" - - cmake .. -DCMAKE_CXX_COMPILER=${COMPILER} -DCMAKE_BUILD_TYPE=${BUILD_TYPE} ${BOTAN} ${CARES} ${SYSTEMD} ${LIBIDN} ${LITESQL} - - make biboumi -j$(nproc) - - make check -j$(nproc) - -image: biboumi-test-fedora:latest - -build:1: - variables: - BOTAN: "-DWITHOUT_BOTAN=1" - <<: *basic_build - -build:2: - variables: - CARES: "-DWITHOUT_CARES=1" - <<: *basic_build - -build:3: - variables: - LITESQL: "-DWITHOUT_LITESQL=1" - <<: *basic_build - -build:4: - variables: - LITESQL: "-DWITHOUT_LITESQL=1" - BOTAN: "-DWITHOUT_BOTAN=1" - <<: *basic_build - -build:5: - variables: - LITESQL: "-DWITHOUT_LITESQL=1" - CARES: "-DWITHOUT_CARES=1" - <<: *basic_build - -build:6: - variables: - BOTAN: "-DWITHOUT_BOTAN=1" - CARES: "-DWITHOUT_CARES=1" - <<: *basic_build - -build:6: - variables: - LIBIDN: "-DWITHOUT_LIBIDN=1" - CARES: "-DWITHOUT_CARES=1" - <<: *basic_build - -build:rpm: - stage: build - script: - - cmake .. -DCMAKE_CXX_COMPILER=${COMPILER} -DCMAKE_BUILD_TYPE=${BUILD_TYPE} ${BOTAN} ${CARES} ${SYSTEMD} ${LIBIDN} ${LITESQL} - - make rpm -j$(nproc) - artifacts: - paths: - - build/rpmbuild/RPMS - - build/rpmbuild/SRPMS - when: always - - -.template:basic_test: &basic_test - stage: test - script: - - cmake .. -DCMAKE_CXX_COMPILER=${COMPILER} -DCMAKE_BUILD_TYPE=${BUILD_TYPE} ${BOTAN} ${CARES} ${SYSTEMD} ${LIBIDN} ${LITESQL} - - make biboumi -j$(nproc) - - make check - - make coverage - - mkdir tests_outputs && pushd tests_outputs && make e2e -j$(nproc) -C .. && popd - artifacts: - paths: - - build/coverage/ - - build/tests_outputs/ - when: always - -test:debian: - stage: test - image: biboumi-test-debian:latest - <<: *basic_test - -test:fedora: - stage: test - image: biboumi-test-fedora:latest - <<: *basic_test
\ No newline at end of file diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 75a96cc..a896e68 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,3 +1,22 @@ +Version 4.0 - 2016-11-09 +======================== + + - The separator between the IRC nickname and the IRC server is now '%' + instead of '!'. This makes things simpler (only one separator to + remember). The distinction between a JID referring to a channel and a JID + refering to a nickname is based on the first character (# or & by + default, but this can be customized by the server with the ISUPPORT + extension). + - Handle channel invitations in both directions. + - Add support for `JID escaping <.http://www.xmpp.org/extensions/xep-0106.html>`. + - Save all channel messages into the database, with an ad-hoc option to + disable this feature. + - When joining a room, biboumi sends an history of the most recents messages + found in the database. + - Channel history can be retrieved using Message Archive Management. + - Result Set Management can be used to request only parts of the IRC channel + list. + Version 3.0 - 2016-08-03 ======================== diff --git a/CMakeLists.txt b/CMakeLists.txt index d6d5ce8..2301123 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -2,7 +2,7 @@ cmake_minimum_required(VERSION 3.0) project(biboumi) -set(${PROJECT_NAME}_VERSION_MAJOR 3) +set(${PROJECT_NAME}_VERSION_MAJOR 4) set(${PROJECT_NAME}_VERSION_MINOR 0) set(${PROJECT_NAME}_VERSION_SUFFIX "") @@ -96,7 +96,7 @@ if(LITESQL_FOUND) add_library(database STATIC src/database/database.cpp ${LITESQL_GENERATED_SOURCES}) - target_link_libraries(database ${LITESQL_LIBRARIES}) + target_link_libraries(database ${LITESQL_LIBRARIES} utils) if(BOTAN_FOUND) target_link_libraries(database ${BOTAN_LIBRARIES}) endif() @@ -215,7 +215,7 @@ if(NOT EXISTS ${CMAKE_SOURCE_DIR}/tests/catch.hpp) ) add_dependencies(test_suite catch) endif() -add_custom_target(check COMMAND "test_suite" "-s" +add_custom_target(check COMMAND "test_suite" DEPENDS test_suite biboumi) add_custom_target(e2e COMMAND "python3" "${CMAKE_CURRENT_SOURCE_DIR}/tests/end_to_end/" DEPENDS biboumi) @@ -232,6 +232,10 @@ if(CMAKE_BUILD_TYPE MATCHES Debug) test_suite coverage ) + SETUP_TARGET_FOR_COVERAGE(coverage_e2e + make + coverage_e2e + e2e) endif() # @@ -246,8 +250,8 @@ install(FILES conf/biboumi.cfg DESTINATION /etc/bib ## Dist target ## Generate a release tarball from the git sources # -add_custom_target(dist - COMMAND git archive --prefix=${ARCHIVE_NAME}/ --format=tar HEAD +add_custom_command(OUTPUT ${ARCHIVE_NAME}.tar.xz + COMMAND git archive --prefix=${ARCHIVE_NAME}/ --format=tar HEAD^{tree} > ${CMAKE_CURRENT_BINARY_DIR}/${ARCHIVE_NAME}.tar # Append this specific file that is not part of the git repo COMMAND tar -rf ${CMAKE_CURRENT_BINARY_DIR}/${ARCHIVE_NAME}.tar -P ${SOURCE_DIR}/single_include/catch.hpp --xform 's|/.*/|${ARCHIVE_NAME}/tests/|g' @@ -258,13 +262,15 @@ add_custom_target(dist COMMAND ${CMAKE_COMMAND} -E cmake_echo_color --cyan "${ARCHIVE_NAME}.tar.xz created." WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} ) -add_dependencies(dist catch) +add_custom_target(dist + DEPENDS ${ARCHIVE_NAME}.tar.xz + DEPENDS catch) add_custom_target(rpm + DEPENDS dist COMMAND mkdir -p rpmbuild/{BUILD,RPMS,SOURCES,SPECS,SRPMS} COMMAND rpmbuild --define "_topdir `pwd`/rpmbuild/" --define "_sourcedir `pwd`" -ba biboumi.spec ) -add_dependencies(rpm dist) if(BOTAN_FOUND) set(STR_WITH_BOTAN "Botan: yes") diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst new file mode 100644 index 0000000..ed3915f --- /dev/null +++ b/CONTRIBUTING.rst @@ -0,0 +1,52 @@ +Contributing to biboumi +======================= + +Biboumi’s main workplace is at https://lab.louiz.org/louiz/biboumi + +The repository is also mirrored on other websites, for example on github, +but that’s mainly for the convenience of users. + +Before doing anything, you can come on the `XMPP chatroom`_ to discuss your +changes, issues or ideas. + +Bug reports, feature requests +----------------------------- + +To open a bug report, or a feature request, please do so on `our gitlab’s +bug tracker`_. + +If the issue you’re reporting may have security implications, please select +the “confidential” flag in your bug report. + + +Code +---- + +To contribute code, you can do so using git: commit your changes on any +publicly available git repository and communicate us its address. This can +be done with a `gitlab merge request`_, or a `github pull request`_ or just +by sending a message into the `XMPP chatroom`_. + +It is suggested that you use gitlab’s merge requests: this will +automatically run our continuous integration tests. + +It is also recommended to add some unit or end-to-end tests for the proposed +changes. + + +Coding style +------------ +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. +- Type names are in PascalCase. +- Everything else is in snake_case. + + +.. _our gitlab’s bug tracker: https://lab.louiz.org/louiz/biboumi/issues/new +.. _gitlab merge request: https://lab.louiz.org/louiz/biboumi/merge_requests/new +.. _github pull request: https://github.com/louiz/biboumi/pulls +.. _XMPP chatroom: xmpp:biboumi@muc.poez.io @@ -1,5 +1,3 @@ -Copyright (c) 2015 Florent Le Coz - This software is provided 'as-is', without any express or implied warranty. In no event will the authors be held liable for any damages arising from the use of this software. @@ -1,6 +1,21 @@ Biboumi ======= +.. image:: https://lab.louiz.org/louiz/biboumi/badges/master/build.svg + :target: https://lab.louiz.org/louiz/biboumi/commits/master + +.. image:: https://lab.louiz.org/louiz/biboumi/badges/master/coverage.svg + :target: https://lab.louiz.org/louiz/biboumi/commits/master + +.. image:: https://sonarqube.com/api/badges/gate?key=biboumi + :target: https://sonarqube.com/component_issues/index?id=biboumi + +.. image:: https://scan.coverity.com/projects/3726/badge.svg + :target: https://scan.coverity.com/projects/louiz-biboumi + +.. image:: https://bestpractices.coreinfrastructure.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. @@ -29,6 +44,7 @@ Usage ----- Read `the documentation`_. + Authors ------- Florent Le Coz (louiz’) <louiz@louiz.org> @@ -37,17 +53,9 @@ Florent Le Coz (louiz’) <louiz@louiz.org> Contact/Support --------------- * XMPP ChatRoom: biboumi@muc.poez.io -* Report a bug: https://dev.louiz.org/projects/biboumi/issues/new - -To contribute, the preferred way is to commit your changes on some -publicly-available git repository (your own, or github -(https://github.com/louiz/biboumi), or a fork on https://lab.louiz.org) and -to notify the developers with a ticket on the bug tracker -(https://dev.louiz.org/projects/biboumi/issues/new), a pull request on -github or a merge request on gitlab. +* Report a bug: https://lab.louiz.org/louiz/biboumi/issues/new -Optionally you can come discuss your changes on the XMPP chat room, -beforehand. +Also, see the `contributing`_ page. Licence @@ -60,3 +68,4 @@ Please read the COPYING file for details. .. _INSTALL: INSTALL.rst .. _the documentation: doc/biboumi.1.rst +.. _contributing: CONTRIBUTING.rst diff --git a/cmake/Modules/CodeCoverage.cmake b/cmake/Modules/CodeCoverage.cmake index 4f54327..c07a3df 100644 --- a/cmake/Modules/CodeCoverage.cmake +++ b/cmake/Modules/CodeCoverage.cmake @@ -47,23 +47,23 @@ # # 3. Set compiler flags to turn off optimization and enable coverage: # SET(CMAKE_CXX_FLAGS "-g -O0 -fprofile-arcs -ftest-coverage") -# SET(CMAKE_C_FLAGS "-g -O0 -fprofile-arcs -ftest-coverage") +# SET(CMAKE_C_FLAGS "-g -O0 -fprofile-arcs -ftest-coverage") # # 3. Use the function SETUP_TARGET_FOR_COVERAGE to create a custom make target # which runs your test executable and produces a lcov code coverage report: # Example: -# SETUP_TARGET_FOR_COVERAGE( -# my_coverage_target # Name for custom target. -# test_driver # Name of the test driver executable that runs the tests. -# # NOTE! This should always have a ZERO as exit code -# # otherwise the coverage generation will not complete. -# coverage # Name of output directory. -# ) +# SETUP_TARGET_FOR_COVERAGE( +# my_coverage_target # Name for custom target. +# test_driver # Name of the test driver executable that runs the tests. +# # NOTE! This should always have a ZERO as exit code +# # otherwise the coverage generation will not complete. +# coverage # Name of output directory. +# ) # # 4. Build a Debug build: -# cmake -DCMAKE_BUILD_TYPE=Debug .. -# make -# make my_coverage_target +# cmake -DCMAKE_BUILD_TYPE=Debug .. +# make +# make my_coverage_target # # @@ -76,9 +76,9 @@ FIND_PROGRAM( GCOVR_PATH gcovr PATHS ${CMAKE_SOURCE_DIR}/tests) # Display an error when the target is called. If no error is found, this # function will be overridden by the real one later in this file FUNCTION(SETUP_TARGET_FOR_COVERAGE _targetname _testrunner _outputname) - ADD_CUSTOM_TARGET(${_targetname} + ADD_CUSTOM_TARGET(${_targetname} COMMAND echo "Coverage is not available: ${ERROR_MSG}" - ) + ) ENDFUNCTION() IF(NOT GCOV_PATH) @@ -130,50 +130,50 @@ ENDIF() # NOT CMAKE_BUILD_TYPE STREQUAL "Debug" # Param _targetname The name of new the custom make target # Param _testrunner The name of the target which runs the tests. -# MUST return ZERO always, even on errors. -# If not, no coverage report will be created! +# MUST return ZERO always, even on errors. +# If not, no coverage report will be created! # Param _outputname lcov output is generated as _outputname.info # HTML report is generated in _outputname/index.html # Optional fifth parameter is passed as arguments to _testrunner # Pass them in list form, e.g.: "-j;2" for -j 2 FUNCTION(SETUP_TARGET_FOR_COVERAGE _targetname _testrunner _outputname) - # Setup target - ADD_CUSTOM_TARGET(${_targetname} + # Setup target + ADD_CUSTOM_TARGET(${_targetname} - # Cleanup lcov - COMMAND ${LCOV_PATH} --directory . --zerocounters + # Cleanup lcov + COMMAND ${LCOV_PATH} --directory . --zerocounters - # Create baseline coverage data file - COMMAND ${LCOV_PATH} -c -i -d . -o ${_outputname}.baseline.info -q + # Create baseline coverage data file + COMMAND ${LCOV_PATH} -c -i -d . -o ${_outputname}.baseline.info -q - # Run tests - COMMAND ${_testrunner} ${ARGV3} + # Run tests + COMMAND ${_testrunner} ${ARGV3} - # Capturing lcov counters and generating report - COMMAND ${LCOV_PATH} --directory . --capture --output-file ${_outputname}.info -q - # Combine the baseline and the test data - COMMAND ${LCOV_PATH} -a ${_outputname}.info -a ${_outputname}.baseline.info -o ${_outputname}.info -q + # Capturing lcov counters and generating report + COMMAND ${LCOV_PATH} --directory . --capture --output-file ${_outputname}.info -q + # Combine the baseline and the test data + COMMAND ${LCOV_PATH} -a ${_outputname}.info -a ${_outputname}.baseline.info -o ${_outputname}.info -q - # 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/*' --output-file ${_outputname}.info.cleaned -q + # 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.cleaned -q - # Generate the report - COMMAND ${GENHTML_PATH} -o ${_outputname} ${_outputname}.info.cleaned + # Generate the report + COMMAND ${GENHTML_PATH} -o ${_outputname} ${_outputname}.info.cleaned - # Clean the temporary files we created - COMMAND ${CMAKE_COMMAND} -E remove ${_outputname}.info ${_outputname}.info.cleaned + # Clean the temporary files we created + COMMAND ${CMAKE_COMMAND} -E remove ${_outputname}.info ${_outputname}.info.cleaned - WORKING_DIRECTORY ${CMAKE_BINARY_DIR} - COMMENT "Resetting code coverage counters to zero.\nProcessing code coverage counters and generating report." - ) + WORKING_DIRECTORY ${CMAKE_BINARY_DIR} + COMMENT "Resetting code coverage counters to zero.\nProcessing code coverage counters and generating report." + ) - # Show info where to find the report - ADD_CUSTOM_COMMAND(TARGET ${_targetname} POST_BUILD - COMMAND ; - COMMENT "Open ./${_outputname}/index.html in your browser to view the coverage report." - ) + # Show info where to find the report + ADD_CUSTOM_COMMAND(TARGET ${_targetname} POST_BUILD + COMMAND ; + COMMENT "Open ./${_outputname}/index.html in your browser to view the coverage report." + ) ENDFUNCTION() # SETUP_TARGET_FOR_COVERAGE @@ -184,30 +184,30 @@ ENDFUNCTION() # SETUP_TARGET_FOR_COVERAGE # Pass them in list form, e.g.: "-j;2" for -j 2 FUNCTION(SETUP_TARGET_FOR_COVERAGE_COBERTURA _targetname _testrunner _outputname) - IF(NOT PYTHON_EXECUTABLE) - MESSAGE(FATAL_ERROR "Python not found! Aborting...") - ENDIF() # NOT PYTHON_EXECUTABLE + IF(NOT PYTHON_EXECUTABLE) + MESSAGE(FATAL_ERROR "Python not found! Aborting...") + ENDIF() # NOT PYTHON_EXECUTABLE - IF(NOT GCOVR_PATH) - MESSAGE(FATAL_ERROR "gcovr not found! Aborting...") - ENDIF() # NOT GCOVR_PATH + IF(NOT GCOVR_PATH) + MESSAGE(FATAL_ERROR "gcovr not found! Aborting...") + ENDIF() # NOT GCOVR_PATH MARK_AS_ADVANCED(GCOVR_PATH) - ADD_CUSTOM_TARGET(${_targetname} + ADD_CUSTOM_TARGET(${_targetname} - # Run tests - ${_testrunner} ${ARGV3} + # Run tests + ${_testrunner} ${ARGV3} - # Running gcovr - COMMAND ${GCOVR_PATH} -x -r ${CMAKE_SOURCE_DIR} -e '${CMAKE_SOURCE_DIR}/tests/' -o ${_outputname}.xml - WORKING_DIRECTORY ${CMAKE_BINARY_DIR} - COMMENT "Running gcovr to produce Cobertura code coverage report." - ) + # Running gcovr + COMMAND ${GCOVR_PATH} -x -r ${CMAKE_SOURCE_DIR} -e '${CMAKE_SOURCE_DIR}/tests/' -o ${_outputname}.xml + WORKING_DIRECTORY ${CMAKE_BINARY_DIR} + COMMENT "Running gcovr to produce Cobertura code coverage report." + ) - # Show info where to find the report - ADD_CUSTOM_COMMAND(TARGET ${_targetname} POST_BUILD - COMMAND ; - COMMENT "Cobertura code coverage report saved in ${_outputname}.xml." - ) + # Show info where to find the report + ADD_CUSTOM_COMMAND(TARGET ${_targetname} POST_BUILD + COMMAND ; + COMMENT "Cobertura code coverage report saved in ${_outputname}.xml." + ) ENDFUNCTION() # SETUP_TARGET_FOR_COVERAGE_COBERTURA diff --git a/database/database.xml b/database/database.xml index f102db0..af15ad5 100644 --- a/database/database.xml +++ b/database/database.xml @@ -2,6 +2,16 @@ <!DOCTYPE database SYSTEM "litesql.dtd"> <database name="BibouDB" namespace="db"> + <object name="GlobalOptions"> + <field name="owner" type="string" length="3071"/> + + <field name="maxHistoryLength" type="integer" default="20"/> + <field name="recordHistory" type="boolean" default="true"/> + <index unique="true"> + <indexfield name="owner"/> + </index> + </object> + <object name="IrcServerOptions"> <field name="owner" type="string" length="3071"/> <field name="server" type="string" length="3071"/> @@ -18,6 +28,7 @@ <field name="encodingOut" type="string" default="ISO-8859-1"/> <field name="encodingIn" type="string" default="ISO-8859-1"/> + <field name="maxHistoryLength" type="integer" default="20"/> <index unique="true"> <indexfield name="owner"/> <indexfield name="server"/> @@ -32,6 +43,8 @@ <field name="encodingOut" type="string"/> <field name="encodingIn" type="string"/> + <field name="maxHistoryLength" type="integer" default="20"/> + <index unique="true"> <indexfield name="owner"/> <indexfield name="server"/> @@ -39,8 +52,17 @@ </index> </object> - <object name="LogLine"> - <field name="date" type="date" /> - <field name="body" type="string" length="4096"/> + <object name="MucLogLine"> + <field name="uuid" type="string" length="36" /> + <!-- The bare JID of the user for which we stored the line. It's + the JID associated with the Bridge --> + <field name="owner" type="string" length="4096" /> + <!-- The room IID --> + <field name="ircChanName" type="string" length="4096" /> + <field name="ircServerName" type="string" length="4096" /> + + <field name="date" type="datetime" /> + <field name="body" type="string" length="65536"/> + <field name="nick" type="string" length="4096" /> </object> </database> diff --git a/doc/biboumi.1.rst b/doc/biboumi.1.rst index dd365f0..49c0fe4 100644 --- a/doc/biboumi.1.rst +++ b/doc/biboumi.1.rst @@ -19,7 +19,7 @@ XMPP client as if these channels were XMPP MUCs. Synopsis ======== -biboumi [*config_filename*\ ] +biboumi [*config_filename*] Options ======= @@ -86,16 +86,13 @@ irc.example.org), then biboumi will enforce the connexion to that IRC server only. This means that a JID like "#chan@biboumi.example.com" must be used instead of "#chan%irc.example.org@biboumi.example.com". In that mode, the virtual channel (see `Connect to an IRC server`_) is not -available and you still need to use the ! separator to send message to an -IRC user (for example "foo!@biboumi.example.com" to send a message to -foo), although the in-room JID still work as expected -("#channel@biboumi.example.com/Nick"). On the other hand, the '%' lose -any meaning. 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. +available. The '%' character loses any meaning in the JIDs. It can appear +in the JID but will not be interpreted as a separator (thus the JID +"#channel%hello@biboumi.example.com" points to the channel named +"#channel%hello" on the configured IRC server) This option can for example +be used by an administrator that just wants to let their users join their own +IRC server using an XMPP client, while forbidding access to any other IRC +server. realname_customization ---------------------- @@ -142,7 +139,7 @@ default is 0, but a more practical value for production use is 1. ca_file ------- -Specifies which file should be use as the list of trusted CA when +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. @@ -201,26 +198,21 @@ 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 have a local part formed like this: -``channel_name`` % ``irc_server``. +IRC channels and IRC users have a local part formed like this: +``name`` % ``irc_server``. -If the IRC channel you want to adress starts with the ``'#'`` character (or an -other character, announced by the IRC server, like ``'&'``, ``'+'`` or ``'!'``), -then you must include it in the JID. Some other gateway implementations, as -well as some IRC clients, do not require them to be started by one of these -characters, adding an implicit ``'#'`` in that case. Biboumi does not do that -because this gets confusing when trying to understand the difference between -the channels *#foo*, and *##foo*. Note that biboumi does not use the -presence of these special characters to identify an IRC channel, only the -presence of the separator `%` is used for that. +``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. -The channel name can also be empty (for example ``%irc.example.com``), in that -case this represents the virtual channel provided by biboumi. See *Connect -to an IRC server* for more details. +As a special case, the channel name can also be empty (for example +``%irc.example.com``), in that case this represents the virtual channel +provided by biboumi. See *Connect to an IRC server* for more details. There is two ways to address an IRC user, using a local part like this: -``nickname`` ! ``irc_server`` -or by using the in-room address of the participant, like this: +``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 @@ -232,11 +224,17 @@ 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 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 have to use a jid like +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. +``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: @@ -244,7 +242,7 @@ Examples: 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%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. @@ -255,9 +253,6 @@ Examples: Note: Some JIDs are valid but make no sense in the context of biboumi: -* ``!irc.example.com@biboumi.example.com`` is the empty-string nick on the - irc.example.com server. It makes no sense to try to send messages to it. - * ``#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. @@ -286,7 +281,7 @@ channel ``#foo`` on the server ``irc.example.com``, but you need to authenticate to a bot of the server before you’re allowed to join it, you can first join the room ``%irc.example.com@biboumi.example.com`` (this will effectively connect you to the IRC server without joining any room), then send your -authentication message to the user ``bot!irc.example.com@biboumi.example.com`` +authentication message to the user ``bot%irc.example.com@biboumi.example.com`` and finally join the room ``#foo%irc.example.com@biboumi.example.com``. Channel messages @@ -299,6 +294,29 @@ messages to other users. This means that the order of the messages displayed in your XMPP client may not be the same than 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 for that user `Ad-hoc commands`. +Private messages (messages that are sent directly to a nickname, not a +channel) are never stored in the database. When a channel is joined, biboumi +sends the `max_history_length` messages found in the database as the MUC +history. + +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. + +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 ------------- @@ -333,7 +351,7 @@ 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 +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. @@ -344,6 +362,21 @@ Notices Notices are received exactly like private messages. It is not possible to send a notice. +Invitations +----------- + +Biboumi forwards the mediated invitations to the target nick. If the user +wishes to invite the user “FooBar” into a room, they can invite one of the +following “JIDs” (one of them is not a JID, actually): + +- foobar%anything@anything +- anything@anything/FooBar +- FooBar + +Note that the “anything” part are simply ignored because they have no +meaning for the IRC server: we already know which IRC server is targeted +using the JID of the target channel. + Kicks and bans -------------- @@ -405,7 +438,7 @@ between IRC modes and XMPP features is as follow: 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``. + 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. @@ -455,13 +488,18 @@ On the gateway itself (e.g on the JID biboumi.example.com): 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 +- configure: Lets each user configure some options that applies to the concerned IRC server. +- 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 +- 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 @@ -492,7 +530,7 @@ 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 serveres. It will first try ports 6697 and 6670 and use TLS if +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. diff --git a/doc/example.conf b/doc/example.conf new file mode 100644 index 0000000..42631b2 --- /dev/null +++ b/doc/example.conf @@ -0,0 +1,14 @@ +# 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/docker/biboumi-test/debian/Dockerfile b/docker/biboumi-test/debian/Dockerfile index f2a26ea..9aac3ec 100644 --- a/docker/biboumi-test/debian/Dockerfile +++ b/docker/biboumi-test/debian/Dockerfile @@ -39,6 +39,8 @@ RUN rm -rf /litesql RUN ldconfig # Install slixmpp, for e2e tests +RUN git clone https://github.com/saghul/aiodns.git +RUN cd aiodns && python3 setup.py build && python3 setup.py install RUN apt install -y python3-pip RUN git clone git://git.louiz.org/slixmpp RUN pip3 install pyasn1 @@ -48,10 +50,10 @@ RUN cd slixmpp && python3 setup.py build && python3 setup.py install RUN useradd tester -m # Install charybdis, for e2e tests -RUN apt install -y automake autoconf flex bison libltdl-dev openssl +RUN apt install -y automake autoconf flex bison libltdl-dev openssl zlib1g-dev RUN apt install -y libtool RUN git clone https://github.com/charybdis-ircd/charybdis.git && cd charybdis -RUN cd /charybdis && ./autogen.sh && ./configure --prefix=/home/tester/ircd --bindir=/usr/bin && make -j8 && make install +RUN cd /charybdis && git checkout 4f2b9a4 && ./autogen.sh && ./configure --prefix=/home/tester/ircd --bindir=/usr/bin && make -j8 && make install RUN chown -R tester:tester /home/tester/ircd RUN rm -rf /charybdis diff --git a/docker/biboumi-test/fedora/Dockerfile b/docker/biboumi-test/fedora/Dockerfile index 3c48645..ebcb4e4 100644 --- a/docker/biboumi-test/fedora/Dockerfile +++ b/docker/biboumi-test/fedora/Dockerfile @@ -53,12 +53,17 @@ RUN useradd tester RUN dnf install -y automake autoconf flex flex-devel bison libtool-ltdl-devel openssl-devel RUN dnf install -y libtool RUN git clone https://github.com/charybdis-ircd/charybdis.git && cd charybdis -RUN cd /charybdis && ./autogen.sh && ./configure --prefix=/home/tester/ircd --bindir=/usr/bin && make -j8 && make install +RUN cd /charybdis && git checkout 4f2b9a4 && ./autogen.sh && ./configure --prefix=/home/tester/ircd --bindir=/usr/bin --with-included-boost && make -j8 && make install RUN chown -R tester:tester /home/tester/ircd RUN rm -rf /charybdis RUN su - tester -c "echo export LANG=en_GB.utf-8 >> /home/tester/.bashrc" +COPY coverity /home/tester/coverity +COPY sonar-scanner-2.8 /home/tester/sonar-scanner + +RUN dnf install -y which java-1.8.0-openjdk + WORKDIR /home/tester USER tester diff --git a/louloulibs/CMakeLists.txt b/louloulibs/CMakeLists.txt index bf53504..908c35f 100644 --- a/louloulibs/CMakeLists.txt +++ b/louloulibs/CMakeLists.txt @@ -143,4 +143,25 @@ if(SYSTEMD_FOUND) target_link_libraries(xmpplib ${SYSTEMD_LIBRARIES}) endif() +# +## Check if we have std::get_time +# +include(CheckCXXSourceCompiles) + +check_cxx_source_compiles(" + #include <iomanip> + int main() + { std::get_time(nullptr, \"\"); }" + HAS_GET_TIME) + +mark_as_advanced(HAS_GET_TIME) + +check_cxx_source_compiles(" + #include <iomanip> + int main() + { std::put_time(nullptr, \"\"); }" + HAS_PUT_TIME) + +mark_as_advanced(HAS_PUT_TIME) + configure_file(${CMAKE_CURRENT_SOURCE_DIR}/louloulibs.h.cmake ${CMAKE_BINARY_DIR}/src/louloulibs.h) diff --git a/louloulibs/cmake/Modules/FindLIBUUID.cmake b/louloulibs/cmake/Modules/FindLIBUUID.cmake index 17d3c42..f344249 100644 --- a/louloulibs/cmake/Modules/FindLIBUUID.cmake +++ b/louloulibs/cmake/Modules/FindLIBUUID.cmake @@ -19,7 +19,7 @@ include(FindPkgConfig) pkg_check_modules(LIBUUID uuid) if(NOT LIBUUID_FOUND) - find_path(LIBUUID_INCLUDE_DIRS NAMES uuid.h + find_path(LIBUUID_INCLUDE_DIRS NAMES uuid/uuid.h PATH_SUFFIXES uuid DOC "The libuuid include directory") diff --git a/louloulibs/louloulibs.h.cmake b/louloulibs/louloulibs.h.cmake index 2feaf4e..6131b70 100644 --- a/louloulibs/louloulibs.h.cmake +++ b/louloulibs/louloulibs.h.cmake @@ -6,4 +6,6 @@ #cmakedefine BOTAN_FOUND #cmakedefine CARES_FOUND #cmakedefine SOFTWARE_VERSION "${SOFTWARE_VERSION}" -#cmakedefine PROJECT_NAME "${PROJECT_NAME}"
\ No newline at end of file +#cmakedefine PROJECT_NAME "${PROJECT_NAME}" +#cmakedefine HAS_GET_TIME +#cmakedefine HAS_PUT_TIME diff --git a/louloulibs/network/credentials_manager.cpp b/louloulibs/network/credentials_manager.cpp index ee83c3b..ed04d24 100644 --- a/louloulibs/network/credentials_manager.cpp +++ b/louloulibs/network/credentials_manager.cpp @@ -29,7 +29,7 @@ BasicCredentialsManager::BasicCredentialsManager(const TCPSocketHandler* const s socket_handler(socket_handler), trusted_fingerprint{} { - this->load_certs(); + BasicCredentialsManager::load_certs(); } void BasicCredentialsManager::set_trusted_fingerprint(const std::string& fingerprint) @@ -62,17 +62,8 @@ void BasicCredentialsManager::verify_certificate_chain(const std::string& type, } } -void BasicCredentialsManager::load_certs() +bool BasicCredentialsManager::try_to_open_one_ca_bundle(const std::vector<std::string>& paths) { - // Only load the certificates the first time - if (BasicCredentialsManager::certs_loaded) - return; - const std::string conf_path = Config::get("ca_file", ""); - std::vector<std::string> paths; - if (conf_path.empty()) - paths = default_cert_files; - else - paths.push_back(conf_path); for (const auto& path: paths) { try @@ -87,25 +78,39 @@ void BasicCredentialsManager::load_certs() // will be ignored. As a result, some TLS connection may be refused // because the certificate is signed by an issuer that was ignored. try { - const Botan::X509_Certificate cert(bundle); - BasicCredentialsManager::certificate_store.add_certificate(cert); - } catch (const Botan::Decoding_Error& error) - { + Botan::X509_Certificate cert(bundle); + BasicCredentialsManager::certificate_store.add_certificate(std::move(cert)); + } catch (const Botan::Decoding_Error& error) { continue; } } // Only use the first file that can successfully be read. - goto success; + return true; } - catch (Botan::Stream_IO_Error& e) + catch (const Botan::Stream_IO_Error& e) { log_debug(e.what()); } } - // If we could not open one of the files, print a warning - log_warning("The CA could not be loaded, TLS negociation will probably fail."); - success: - BasicCredentialsManager::certs_loaded = true; + return false; +} + +void BasicCredentialsManager::load_certs() +{ + // Only load the certificates the first time + if (BasicCredentialsManager::certs_loaded) + return; + const std::string conf_path = Config::get("ca_file", ""); + std::vector<std::string> paths; + if (conf_path.empty()) + paths = default_cert_files; + else + paths.push_back(conf_path); + + if (BasicCredentialsManager::try_to_open_one_ca_bundle(paths)) + BasicCredentialsManager::certs_loaded = true; + else + log_warning("The CA could not be loaded, TLS negociation will probably fail."); } std::vector<Botan::Certificate_Store*> BasicCredentialsManager::trusted_certificate_authorities(const std::string&, const std::string&) diff --git a/louloulibs/network/credentials_manager.hpp b/louloulibs/network/credentials_manager.hpp index 0fc4b89..7557372 100644 --- a/louloulibs/network/credentials_manager.hpp +++ b/louloulibs/network/credentials_manager.hpp @@ -29,6 +29,7 @@ public: private: const TCPSocketHandler* const socket_handler; + static bool try_to_open_one_ca_bundle(const std::vector<std::string>& paths); static void load_certs(); static Botan::Certificate_Store_In_Memory certificate_store; static bool certs_loaded; diff --git a/louloulibs/network/dns_handler.cpp b/louloulibs/network/dns_handler.cpp index e267944..fef0cfc 100644 --- a/louloulibs/network/dns_handler.cpp +++ b/louloulibs/network/dns_handler.cpp @@ -46,11 +46,7 @@ void DNSHandler::destroy() void DNSHandler::gethostbyname(const std::string& name, ares_host_callback callback, void* data, int family) { - if (family == AF_INET) - ::ares_gethostbyname(this->channel, name.data(), family, - callback, data); - else - ::ares_gethostbyname(this->channel, name.data(), family, + ::ares_gethostbyname(this->channel, name.data(), family, callback, data); } diff --git a/louloulibs/network/dns_socket_handler.cpp b/louloulibs/network/dns_socket_handler.cpp index 5fd08cb..403a5be 100644 --- a/louloulibs/network/dns_socket_handler.cpp +++ b/louloulibs/network/dns_socket_handler.cpp @@ -42,7 +42,8 @@ bool DNSSocketHandler::is_connected() const void DNSSocketHandler::remove_from_poller() { - this->poller->remove_socket_handler(this->socket); + if (this->poller->is_managing_socket(this->socket)) + this->poller->remove_socket_handler(this->socket); } #endif /* CARES_FOUND */ diff --git a/louloulibs/network/poller.cpp b/louloulibs/network/poller.cpp index 8a6fd97..9f5bcfb 100644 --- a/louloulibs/network/poller.cpp +++ b/louloulibs/network/poller.cpp @@ -95,7 +95,7 @@ void Poller::remove_socket_handler(const socket_t socket) void Poller::watch_send_events(SocketHandler* socket_handler) { #if POLLER == POLL - for (size_t i = 0; i <= this->nfds; ++i) + for (size_t i = 0; i < this->nfds; ++i) { if (this->fds[i].fd == socket_handler->get_socket()) { @@ -171,7 +171,7 @@ int Poller::poll(const std::chrono::milliseconds& timeout) // We cannot possibly have more ready events than the number of fds we are // watching assert(static_cast<unsigned int>(nb_events) <= this->nfds); - for (size_t i = 0; i <= this->nfds && nb_events != 0; ++i) + for (size_t i = 0; i < this->nfds && nb_events != 0; ++i) { auto socket_handler = this->socket_handlers.at(this->fds[i].fd); if (this->fds[i].revents == 0) @@ -186,7 +186,8 @@ int Poller::poll(const std::chrono::milliseconds& timeout) socket_handler->on_send(); nb_events--; } - else if (this->fds[i].revents & POLLOUT) + else if (this->fds[i].revents & POLLOUT || + this->fds[i].revents & POLLIN) { socket_handler->connect(); nb_events--; @@ -226,3 +227,8 @@ size_t Poller::size() const { return this->socket_handlers.size(); } + +bool Poller::is_managing_socket(const socket_t socket) const +{ + return (this->socket_handlers.find(socket) != this->socket_handlers.end()); +} diff --git a/louloulibs/network/poller.hpp b/louloulibs/network/poller.hpp index fc1a1a1..e39e438 100644 --- a/louloulibs/network/poller.hpp +++ b/louloulibs/network/poller.hpp @@ -74,6 +74,10 @@ public: * Returns the number of SocketHandlers managed by the poller. */ size_t size() const; + /** + * Whether the given socket is managed by the poller + */ + bool is_managing_socket(const socket_t socket) const; private: /** diff --git a/louloulibs/network/resolver.cpp b/louloulibs/network/resolver.cpp index 9d6de23..2987aaa 100644 --- a/louloulibs/network/resolver.cpp +++ b/louloulibs/network/resolver.cpp @@ -2,6 +2,7 @@ #include <network/resolver.hpp> #include <string.h> #include <arpa/inet.h> +#include <cstdlib> using namespace std::string_literals; @@ -116,12 +117,13 @@ void Resolver::fill_ares_addrinfo4(const struct hostent* hostent) current->ai_protocol = 0; current->ai_addrlen = sizeof(struct sockaddr_in); - struct sockaddr_in* addr = new struct sockaddr_in; - addr->sin_family = hostent->h_addrtype; - addr->sin_port = htons(strtoul(this->port.data(), nullptr, 10)); - addr->sin_addr.s_addr = (*address)->s_addr; + struct sockaddr_in* ai_addr = new struct sockaddr_in; + + ai_addr->sin_family = hostent->h_addrtype; + ai_addr->sin_port = htons(std::strtoul(this->port.data(), nullptr, 10)); + ai_addr->sin_addr.s_addr = (*address)->s_addr; - current->ai_addr = reinterpret_cast<struct sockaddr*>(addr); + current->ai_addr = reinterpret_cast<struct sockaddr*>(ai_addr); current->ai_next = nullptr; current->ai_canonname = nullptr; @@ -147,14 +149,14 @@ void Resolver::fill_ares_addrinfo6(const struct hostent* hostent) current->ai_protocol = 0; current->ai_addrlen = sizeof(struct sockaddr_in6); - struct sockaddr_in6* addr = new struct sockaddr_in6; - addr->sin6_family = hostent->h_addrtype; - addr->sin6_port = htons(strtoul(this->port.data(), nullptr, 10)); - ::memcpy(addr->sin6_addr.s6_addr, (*address)->s6_addr, 16); - addr->sin6_flowinfo = 0; - addr->sin6_scope_id = 0; + struct sockaddr_in6* ai_addr = new struct sockaddr_in6; + ai_addr->sin6_family = hostent->h_addrtype; + ai_addr->sin6_port = htons(std::strtoul(this->port.data(), nullptr, 10)); + ::memcpy(ai_addr->sin6_addr.s6_addr, (*address)->s6_addr, sizeof(ai_addr->sin6_addr.s6_addr)); + ai_addr->sin6_flowinfo = 0; + ai_addr->sin6_scope_id = 0; - current->ai_addr = reinterpret_cast<struct sockaddr*>(addr); + current->ai_addr = reinterpret_cast<struct sockaddr*>(ai_addr); current->ai_canonname = nullptr; current->ai_next = prev; diff --git a/louloulibs/network/resolver.hpp b/louloulibs/network/resolver.hpp index afe6e2b..29e6f3a 100644 --- a/louloulibs/network/resolver.hpp +++ b/louloulibs/network/resolver.hpp @@ -11,8 +11,9 @@ #include <sys/socket.h> #include <netdb.h> -struct AddrinfoDeleter +class AddrinfoDeleter { + public: void operator()(struct addrinfo* addr) { #ifdef CARES_FOUND diff --git a/louloulibs/network/socket_handler.hpp b/louloulibs/network/socket_handler.hpp index eeb41fe..ea79a18 100644 --- a/louloulibs/network/socket_handler.hpp +++ b/louloulibs/network/socket_handler.hpp @@ -14,7 +14,7 @@ public: poller(poller), socket(socket) {} - virtual ~SocketHandler() {} + virtual ~SocketHandler() = default; SocketHandler(const SocketHandler&) = delete; SocketHandler(SocketHandler&&) = delete; SocketHandler& operator=(const SocketHandler&) = delete; diff --git a/louloulibs/network/tcp_socket_handler.cpp b/louloulibs/network/tcp_socket_handler.cpp index 5420b1c..1dddde5 100644 --- a/louloulibs/network/tcp_socket_handler.cpp +++ b/louloulibs/network/tcp_socket_handler.cpp @@ -80,7 +80,7 @@ void TCPSocketHandler::init_socket(const struct addrinfo* rp) } if (!rp) log_error("Failed to bind socket to ", this->bind_addr, ": ", - strerror(bind_error)); + strerror(errno)); else log_info("Socket successfully bound to ", this->bind_addr); } @@ -103,8 +103,6 @@ void TCPSocketHandler::connect(const std::string& address, const std::string& po this->port = port; this->use_tls = tls; - utils::ScopeGuard sg; - struct addrinfo* addr_res; if (!this->connecting) @@ -181,6 +179,8 @@ void TCPSocketHandler::connect(const std::string& address, const std::string& po if (this->use_tls) this->start_tls(); #endif + this->connection_date = std::chrono::system_clock::now(); + this->on_connected(); return ; } @@ -294,7 +294,8 @@ void TCPSocketHandler::on_send() // unconsting the content of s is ok, sendmsg will never modify it msg_iov[msg.msg_iovlen].iov_base = const_cast<char*>(s.data()); msg_iov[msg.msg_iovlen].iov_len = s.size(); - if (++msg.msg_iovlen == UIO_FASTIOV) + msg.msg_iovlen++; + if (msg.msg_iovlen == UIO_FASTIOV) break; } ssize_t res = ::sendmsg(this->socket, &msg, MSG_NOSIGNAL); @@ -307,23 +308,24 @@ void TCPSocketHandler::on_send() else { // remove all the strings that were successfully sent. - for (auto it = this->out_buf.begin(); - it != this->out_buf.end();) + auto it = this->out_buf.begin(); + while (it != this->out_buf.end()) { - if (static_cast<size_t>(res) >= (*it).size()) + if (static_cast<size_t>(res) >= it->size()) { - res -= (*it).size(); - it = this->out_buf.erase(it); + res -= 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); + *it = it->substr(res, std::string::npos); break; } } + this->out_buf.erase(this->out_buf.begin(), it); if (this->out_buf.empty()) this->poller->stop_watching_send_events(this); } @@ -397,6 +399,16 @@ bool TCPSocketHandler::is_connecting() const return this->connecting || this->resolver.is_resolving(); } +bool TCPSocketHandler::is_using_tls() const +{ + return this->use_tls; +} + +std::string TCPSocketHandler::get_port() const +{ + return this->port; +} + void* TCPSocketHandler::get_receive_buffer(const size_t) const { return nullptr; @@ -418,15 +430,14 @@ void TCPSocketHandler::start_tls() void TCPSocketHandler::tls_recv() { static constexpr size_t buf_size = 4096; - char recv_buf[buf_size]; + Botan::byte recv_buf[buf_size]; const ssize_t size = this->do_recv(recv_buf, buf_size); if (size > 0) { const bool was_active = this->tls->is_active(); try { - this->tls->received_data(reinterpret_cast<const Botan::byte*>(recv_buf), - static_cast<size_t>(size)); + this->tls->received_data(recv_buf, static_cast<size_t>(size)); } catch (const Botan::TLS::TLS_Exception& e) { // May happen if the server sends malformed TLS data (buggy server, // or more probably we are just connected to a server that sends @@ -449,9 +460,8 @@ void TCPSocketHandler::tls_send(std::string&& data) const bool was_active = this->tls->is_active(); if (!this->pre_buf.empty()) { - this->tls->send(reinterpret_cast<const Botan::byte*>(this->pre_buf.data()), - this->pre_buf.size()); - this->pre_buf = ""; + this->tls->send(this->pre_buf.data(), this->pre_buf.size()); + this->pre_buf.clear(); } if (!data.empty()) this->tls->send(reinterpret_cast<const Botan::byte*>(data.data()), @@ -460,7 +470,9 @@ void TCPSocketHandler::tls_send(std::string&& data) this->on_tls_activated(); } else - this->pre_buf += data; + this->pre_buf.insert(this->pre_buf.end(), + std::make_move_iterator(data.begin()), + std::make_move_iterator(data.end())); } void TCPSocketHandler::tls_data_cb(const Botan::byte* data, size_t size) diff --git a/louloulibs/network/tcp_socket_handler.hpp b/louloulibs/network/tcp_socket_handler.hpp index b0ba493..20a3e5a 100644 --- a/louloulibs/network/tcp_socket_handler.hpp +++ b/louloulibs/network/tcp_socket_handler.hpp @@ -13,6 +13,7 @@ #include <netinet/in.h> #include <netdb.h> +#include <chrono> #include <vector> #include <memory> #include <string> @@ -106,6 +107,9 @@ public: #endif bool is_connected() const override final; bool is_connecting() const; + bool is_using_tls() const; + std::string get_port() const; + std::chrono::system_clock::time_point connection_date; private: /** @@ -266,9 +270,6 @@ private: * An additional buffer to keep data that the user wants to send, but * cannot because the handshake is not done. */ - std::string pre_buf; + std::vector<Botan::byte> pre_buf; #endif // BOTAN_FOUND }; - - - diff --git a/louloulibs/utils/encoding.cpp b/louloulibs/utils/encoding.cpp index 507f38a..60f2212 100644 --- a/louloulibs/utils/encoding.cpp +++ b/louloulibs/utils/encoding.cpp @@ -75,13 +75,12 @@ namespace utils std::string remove_invalid_xml_chars(const std::string& original) { // The given string MUST be a valid utf-8 string - unsigned char* res = new unsigned char[original.size()]; - ScopeGuard sg([&res]() { delete[] res;}); + std::vector<char> res(original.size(), '\0'); // pointer where we write valid chars - unsigned char* r = res; + char* r = res.data(); - const unsigned char* str = reinterpret_cast<const unsigned char*>(original.c_str()); + const char* str = original.c_str(); std::bitset<20> codepoint; while (*str) @@ -140,7 +139,7 @@ namespace utils else throw std::runtime_error("Invalid UTF-8 passed to remove_invalid_xml_chars"); } - return std::string(reinterpret_cast<char*>(res), r-res); + return {res.data(), static_cast<size_t>(r - res.data())}; } std::string convert_to_utf8(const std::string& str, const char* charset) @@ -152,7 +151,7 @@ namespace utils throw std::runtime_error("Cannot convert into UTF-8"); // Make sure cd is always closed when we leave this function - ScopeGuard sg([&]{ iconv_close(cd); }); + const auto sg = utils::make_scope_guard([&cd](auto&&){ iconv_close(cd); }); size_t inbytesleft = str.size(); @@ -169,7 +168,7 @@ namespace utils char* outbuf_ptr = outbuf; // Make sure outbuf is always deleted when we leave this function - sg.add_callback([&]{ delete[] outbuf; }); + const auto sg2 = utils::make_scope_guard([outbuf](auto&&){ delete[] outbuf; }); bool done = false; while (done == false) @@ -197,12 +196,8 @@ namespace utils outbuf_ptr++; done = true; break; - case E2BIG: - // This should never happen - done = true; - break; - default: - // This should happen even neverer + case E2BIG: // This should never happen + default: // This should happen even neverer done = true; break; } diff --git a/louloulibs/utils/get_first_non_empty.cpp b/louloulibs/utils/get_first_non_empty.cpp new file mode 100644 index 0000000..5b3bedb --- /dev/null +++ b/louloulibs/utils/get_first_non_empty.cpp @@ -0,0 +1,11 @@ +#include <utils/get_first_non_empty.hpp> + +bool is_empty(const std::string& val) +{ + return val.empty(); +} + +bool is_empty(const int& val) +{ + return val == 0; +} diff --git a/louloulibs/utils/get_first_non_empty.hpp b/louloulibs/utils/get_first_non_empty.hpp new file mode 100644 index 0000000..a38f5fb --- /dev/null +++ b/louloulibs/utils/get_first_non_empty.hpp @@ -0,0 +1,20 @@ +#pragma once + +#include <string> + +bool is_empty(const std::string& val); +bool is_empty(const int& val); + +template <typename T> +T get_first_non_empty(T&& last) +{ + return last; +} + +template <typename T, typename... Args> +T get_first_non_empty(T&& first, Args&&... args) +{ + if (!is_empty(first)) + return first; + return get_first_non_empty(std::forward<Args>(args)...); +} diff --git a/louloulibs/utils/scopeguard.hpp b/louloulibs/utils/scopeguard.hpp index ee1e2ef..cd0e89e 100644 --- a/louloulibs/utils/scopeguard.hpp +++ b/louloulibs/utils/scopeguard.hpp @@ -1,6 +1,7 @@ #pragma once #include <functional> +#include <memory> #include <vector> /** @@ -85,5 +86,11 @@ private: }; +template<typename F> +auto make_scope_guard(F&& f) +{ + return std::unique_ptr<void, std::decay_t<F>>{(void*)1, std::forward<F>(f)}; +} + } diff --git a/louloulibs/utils/sha1.cpp b/louloulibs/utils/sha1.cpp index 76476df..f75bc2a 100644 --- a/louloulibs/utils/sha1.cpp +++ b/louloulibs/utils/sha1.cpp @@ -119,36 +119,3 @@ uint8_t* sha1_result(sha1nfo *s) { // Return pointer to hash (20 characters) return s->state.b; } - -#define HMAC_IPAD 0x36 -#define HMAC_OPAD 0x5c - -void sha1_initHmac(sha1nfo *s, const uint8_t* key, int keyLength) { - uint8_t i; - memset(s->keyBuffer, 0, BLOCK_LENGTH); - if (keyLength > BLOCK_LENGTH) { - // Hash long keys - sha1_init(s); - for (;keyLength--;) sha1_writebyte(s, *key++); - memcpy(s->keyBuffer, sha1_result(s), HASH_LENGTH); - } else { - // Block length keys are used as is - memcpy(s->keyBuffer, key, keyLength); - } - // Start inner hash - sha1_init(s); - for (i=0; i<BLOCK_LENGTH; i++) { - sha1_writebyte(s, s->keyBuffer[i] ^ HMAC_IPAD); - } -} - -uint8_t* sha1_resultHmac(sha1nfo *s) { - uint8_t i; - // Complete inner hash - memcpy(s->innerHash,sha1_result(s),HASH_LENGTH); - // Calculate outer hash - sha1_init(s); - for (i=0; i<BLOCK_LENGTH; i++) sha1_writebyte(s, s->keyBuffer[i] ^ HMAC_OPAD); - for (i=0; i<HASH_LENGTH; i++) sha1_writebyte(s, s->innerHash[i]); - return sha1_result(s); -} diff --git a/louloulibs/utils/sha1.hpp b/louloulibs/utils/sha1.hpp index d02de75..d436782 100644 --- a/louloulibs/utils/sha1.hpp +++ b/louloulibs/utils/sha1.hpp @@ -31,5 +31,3 @@ void sha1_init(sha1nfo *s); void sha1_writebyte(sha1nfo *s, uint8_t data); void sha1_write(sha1nfo *s, const char *data, size_t len); uint8_t* sha1_result(sha1nfo *s); -void sha1_initHmac(sha1nfo *s, const uint8_t* key, int keyLength); -uint8_t* sha1_resultHmac(sha1nfo *s); diff --git a/louloulibs/utils/time.cpp b/louloulibs/utils/time.cpp new file mode 100644 index 0000000..afd6117 --- /dev/null +++ b/louloulibs/utils/time.cpp @@ -0,0 +1,70 @@ +#include <utils/time.hpp> +#include <time.h> + +#include <sstream> +#include <iomanip> +#include <locale> + +#include "louloulibs.h" + +namespace utils +{ +std::string to_string(const std::time_t& timestamp) +{ + constexpr std::size_t stamp_size = 21; + char date_buf[stamp_size]; + if (std::strftime(date_buf, stamp_size, "%FT%TZ", std::gmtime(×tamp)) != stamp_size - 1) + return ""; + return {std::begin(date_buf), std::end(date_buf) - 1}; +} + +std::time_t parse_datetime(const std::string& stamp) +{ + static const char* format = "%Y-%m-%dT%H:%M:%S"; + std::tm t = {}; +#ifdef HAS_GET_TIME + std::istringstream ss(stamp); + ss.imbue(std::locale("en_US.utf-8")); + + std::string timezone; + ss >> std::get_time(&t, format) >> timezone; + if (ss.fail()) + return -1; +#else + /* Y - m - d T H : M : S */ + constexpr std::size_t stamp_size_without_tz = 4 + 1 + 2 + 1 + 2 + 1 + 2 + 1 + 2 + 1 + 2; + if (!strptime(stamp.data(), format, &t)) { + return -1; + } + const std::string timezone(stamp.data() + stamp_size_without_tz); +#endif + + if (timezone.empty()) + return -1; + + if (timezone.compare(0, 1, "Z") != 0) + { + std::stringstream tz_ss; + tz_ss << timezone; + int multiplier = -1; + char prefix; + int hours; + char sep; + int minutes; + tz_ss >> prefix >> hours >> sep >> minutes; + if (tz_ss.fail()) + return -1; + if (prefix == '-') + multiplier = +1; + else if (prefix != '+') + return -1; + + t.tm_hour += multiplier * hours; + t.tm_min += multiplier * minutes; + } + return ::timegm(&t); +} + +} + + diff --git a/louloulibs/utils/time.hpp b/louloulibs/utils/time.hpp new file mode 100644 index 0000000..c71cd9c --- /dev/null +++ b/louloulibs/utils/time.hpp @@ -0,0 +1,10 @@ +#pragma once + +#include <ctime> +#include <string> + +namespace utils +{ +std::string to_string(const std::time_t& timestamp); +std::time_t parse_datetime(const std::string& stamp); +}
\ No newline at end of file diff --git a/louloulibs/xmpp/adhoc_commands_handler.cpp b/louloulibs/xmpp/adhoc_commands_handler.cpp index 17c4e67..540cac0 100644 --- a/louloulibs/xmpp/adhoc_commands_handler.cpp +++ b/louloulibs/xmpp/adhoc_commands_handler.cpp @@ -15,9 +15,12 @@ const std::map<const std::string, const AdhocCommand>& AdhocCommandsHandler::get return this->commands; } -std::map<const std::string, const AdhocCommand>& AdhocCommandsHandler::get_commands() +void AdhocCommandsHandler::add_command(std::string name, AdhocCommand command) { - return this->commands; + const auto found = this->commands.find(name); + if (found != this->commands.end()) + throw std::runtime_error("Trying to add an ad-hoc command that already exist: "s + name); + this->commands.emplace(std::make_pair(std::move(name), std::move(command))); } XmlNode AdhocCommandsHandler::handle_request(const std::string& executor_jid, const std::string& to, XmlNode command_node) diff --git a/louloulibs/xmpp/adhoc_commands_handler.hpp b/louloulibs/xmpp/adhoc_commands_handler.hpp index 91eb5bd..e37d913 100644 --- a/louloulibs/xmpp/adhoc_commands_handler.hpp +++ b/louloulibs/xmpp/adhoc_commands_handler.hpp @@ -31,9 +31,9 @@ public: */ const std::map<const std::string, const AdhocCommand>& get_commands() const; /** - * This one can be used to add new commands. + * Add a command into the list, associated with the given name */ - std::map<const std::string, const AdhocCommand>& get_commands(); + void add_command(std::string name, AdhocCommand command); /** * Find the requested command, create a new session or use an existing * one, and process the request (provide a new form, an error, or a diff --git a/louloulibs/xmpp/auth.cpp b/louloulibs/xmpp/auth.cpp new file mode 100644 index 0000000..c20f95d --- /dev/null +++ b/louloulibs/xmpp/auth.cpp @@ -0,0 +1,21 @@ +#include <xmpp/auth.hpp> + +#include <utils/sha1.hpp> + +#include <iomanip> +#include <sstream> + +std::string get_handshake_digest(const std::string& stream_id, const std::string& secret) +{ + sha1nfo sha1; + sha1_init(&sha1); + sha1_write(&sha1, stream_id.data(), stream_id.size()); + sha1_write(&sha1, secret.data(), secret.size()); + const uint8_t* result = sha1_result(&sha1); + + std::ostringstream digest; + for (int i = 0; i < HASH_LENGTH; i++) + digest << std::hex << std::setfill('0') << std::setw(2) << static_cast<int>(result[i]); + + return digest.str(); +} diff --git a/louloulibs/xmpp/auth.hpp b/louloulibs/xmpp/auth.hpp new file mode 100644 index 0000000..34a2116 --- /dev/null +++ b/louloulibs/xmpp/auth.hpp @@ -0,0 +1,6 @@ +#pragma once + +#include <string> + +std::string get_handshake_digest(const std::string& stream_id, const std::string& secret); + diff --git a/louloulibs/xmpp/jid.hpp b/louloulibs/xmpp/jid.hpp index 08327ef..85e835c 100644 --- a/louloulibs/xmpp/jid.hpp +++ b/louloulibs/xmpp/jid.hpp @@ -26,7 +26,12 @@ public: } std::string full() const { - return this->local + "@" + this->domain + "/" + this->resource; + std::string res = this->domain; + if (!this->local.empty()) + res = this->local + "@" + this->domain; + if (!this->resource.empty()) + res += "/" + this->resource; + return res; } }; diff --git a/louloulibs/xmpp/roster.cpp b/louloulibs/xmpp/roster.cpp deleted file mode 100644 index a14a384..0000000 --- a/louloulibs/xmpp/roster.cpp +++ /dev/null @@ -1,21 +0,0 @@ -#include <xmpp/roster.hpp> - -RosterItem::RosterItem(const std::string& jid, const std::string& name, - std::vector<std::string>& groups): - jid(jid), - name(name), - groups(groups) -{ -} - -RosterItem::RosterItem(const std::string& jid, const std::string& name): - jid(jid), - name(name), - groups{} -{ -} - -void Roster::clear() -{ - this->items.clear(); -} diff --git a/louloulibs/xmpp/roster.hpp b/louloulibs/xmpp/roster.hpp deleted file mode 100644 index aa1b449..0000000 --- a/louloulibs/xmpp/roster.hpp +++ /dev/null @@ -1,71 +0,0 @@ -#pragma once - - -#include <algorithm> -#include <string> -#include <vector> - -class RosterItem -{ -public: - RosterItem(const std::string& jid, const std::string& name, - std::vector<std::string>& groups); - RosterItem(const std::string& jid, const std::string& name); - RosterItem() = default; - ~RosterItem() = default; - RosterItem(const RosterItem&) = default; - RosterItem(RosterItem&&) = default; - RosterItem& operator=(const RosterItem&) = default; - RosterItem& operator=(RosterItem&&) = default; - - std::string jid; - std::string name; - std::vector<std::string> groups; - -private: -}; - -/** - * Keep track of the last known stat of a JID's roster - */ -class Roster -{ -public: - Roster() = default; - ~Roster() = default; - - void clear(); - - template <typename... ArgsType> - RosterItem* add_item(ArgsType&&... args) - { - this->items.emplace_back(std::forward<ArgsType>(args)...); - auto it = this->items.end() - 1; - return &*it; - } - RosterItem* get_item(const std::string& jid) - { - auto it = std::find_if(this->items.begin(), this->items.end(), - [this, &jid](const auto& item) - { - return item.jid == jid; - }); - if (it != this->items.end()) - return &*it; - return nullptr; - } - const std::vector<RosterItem>& get_items() const - { - return this->items; - } - -private: - std::vector<RosterItem> items; - - Roster(const Roster&) = delete; - Roster(Roster&&) = delete; - Roster& operator=(const Roster&) = delete; - Roster& operator=(Roster&&) = delete; -}; - - diff --git a/louloulibs/xmpp/xmpp_component.cpp b/louloulibs/xmpp/xmpp_component.cpp index e87cdf7..fa8b0a5 100644 --- a/louloulibs/xmpp/xmpp_component.cpp +++ b/louloulibs/xmpp/xmpp_component.cpp @@ -5,16 +5,18 @@ #include <xmpp/xmpp_component.hpp> #include <config/config.hpp> +#include <utils/time.hpp> +#include <xmpp/auth.hpp> #include <xmpp/jid.hpp> -#include <utils/sha1.hpp> #include <stdexcept> #include <iostream> #include <set> -#include <stdio.h> +#include <uuid/uuid.h> -#include <uuid.h> +#include <cstdlib> +#include <set> #include <louloulibs.h> #ifdef SYSTEMD_FOUND @@ -136,17 +138,7 @@ void XmppComponent::on_remote_stream_open(const XmlNode& node) } // Try to authenticate - char digest[HASH_LENGTH * 2 + 1]; - sha1nfo sha1; - sha1_init(&sha1); - sha1_write(&sha1, this->stream_id.data(), this->stream_id.size()); - sha1_write(&sha1, this->secret.data(), this->secret.size()); - const uint8_t* result = sha1_result(&sha1); - for (int i=0; i < HASH_LENGTH; i++) - sprintf(digest + (i*2), "%02x", result[i]); - digest[HASH_LENGTH * 2] = '\0'; - - auto data = "<handshake xmlns='" COMPONENT_NS "'>"s + digest + "</handshake>"; + auto data = "<handshake xmlns='" COMPONENT_NS "'>"s + get_handshake_digest(this->stream_id, this->secret) + "</handshake>"; log_debug("XMPP SENDING: ", data); this->send_data(std::move(data)); } @@ -230,9 +222,8 @@ void XmppComponent::close_document() this->doc_open = false; } -void XmppComponent::handle_handshake(const Stanza& stanza) +void XmppComponent::handle_handshake(const Stanza&) { - (void)stanza; this->authenticated = true; this->ever_auth = true; log_info("Authenticated with the XMPP server"); @@ -270,7 +261,8 @@ void* XmppComponent::get_receive_buffer(const size_t size) const return this->parser.get_buffer(size); } -void XmppComponent::send_message(const std::string& from, Xmpp::body&& body, const std::string& to, const std::string& type, const bool fulljid) +void XmppComponent::send_message(const std::string& from, Xmpp::body&& body, const std::string& to, + const std::string& type, const bool fulljid, const bool nocopy) { XmlNode node("message"); node["to"] = to; @@ -291,6 +283,18 @@ void XmppComponent::send_message(const std::string& from, Xmpp::body&& body, con html.add_child(std::move(std::get<1>(body))); node.add_child(std::move(html)); } + + if (nocopy) + { + XmlNode private_node("private"); + private_node["xmlns"] = "urn:xmpp:carbons:2"; + node.add_child(std::move(private_node)); + + XmlNode nocopy("no-copy"); + nocopy["xmlns"] = "urn:xmpp:hints"; + node.add_child(std::move(nocopy)); + } + this->send_stanza(node); } @@ -363,31 +367,6 @@ void XmppComponent::send_invalid_room_error(const std::string& muc_name, this->send_stanza(presence); } -void XmppComponent::send_invalid_user_error(const std::string& user_name, const std::string& to) -{ - Stanza message("message"); - message["from"] = user_name + "@" + this->served_hostname; - message["to"] = to; - message["type"] = "error"; - XmlNode x("x"); - x["xmlns"] = MUC_NS; - message.add_child(std::move(x)); - XmlNode error("error"); - error["type"] = "cancel"; - XmlNode item_not_found("item-not-found"); - item_not_found["xmlns"] = STANZA_NS; - error.add_child(std::move(item_not_found)); - XmlNode text("text"); - text["xmlns"] = STANZA_NS; - text["xml:lang"] = "en"; - text.set_inner(user_name + - " is not a valid IRC user name. A correct user jid is of the form: <nick>!<server>@" + - this->served_hostname); - error.add_child(std::move(text)); - message.add_child(std::move(error)); - this->send_stanza(message); -} - void XmppComponent::send_topic(const std::string& from, Xmpp::body&& topic, const std::string& to, const std::string& who) { XmlNode message("message"); @@ -426,6 +405,29 @@ void XmppComponent::send_muc_message(const std::string& muc_name, const std::str this->send_stanza(message); } +void XmppComponent::send_history_message(const std::string& muc_name, const std::string& nick, const std::string& body_txt, const std::string& jid_to, std::time_t timestamp) +{ + Stanza message("message"); + message["to"] = jid_to; + if (!nick.empty()) + message["from"] = muc_name + "@" + this->served_hostname + "/" + nick; + else + message["from"] = muc_name + "@" + this->served_hostname; + message["type"] = "groupchat"; + + XmlNode body("body"); + body.set_inner(body_txt); + message.add_child(std::move(body)); + + XmlNode delay("delay"); + delay["xmlns"] = DELAY_NS; + delay["from"] = muc_name + "@" + this->served_hostname; + delay["stamp"] = utils::to_string(timestamp); + + message.add_child(std::move(delay)); + this->send_stanza(message); +} + void XmppComponent::send_muc_leave(const std::string& muc_name, std::string&& nick, Xmpp::body&& message, const std::string& jid_to, const bool self) { Stanza presence("presence"); @@ -483,11 +485,8 @@ void XmppComponent::send_nick_change(const std::string& muc_name, this->send_user_join(muc_name, new_nick, "", affiliation, role, jid_to, self); } -void XmppComponent::kick_user(const std::string& muc_name, - const std::string& target, - const std::string& txt, - const std::string& author, - const std::string& jid_to) +void XmppComponent::kick_user(const std::string& muc_name, const std::string& target, const std::string& txt, + const std::string& author, const std::string& jid_to, const bool self) { Stanza presence("presence"); presence["from"] = muc_name + "@" + this->served_hostname + "/" + target; @@ -509,6 +508,12 @@ void XmppComponent::kick_user(const std::string& muc_name, XmlNode status("status"); status["code"] = "307"; x.add_child(std::move(status)); + if (self) + { + XmlNode status("status"); + status["code"] = "110"; + x.add_child(std::move(status)); + } presence.add_child(std::move(x)); this->send_stanza(presence); } diff --git a/louloulibs/xmpp/xmpp_component.hpp b/louloulibs/xmpp/xmpp_component.hpp index 5fc6d2e..5f5f937 100644 --- a/louloulibs/xmpp/xmpp_component.hpp +++ b/louloulibs/xmpp/xmpp_component.hpp @@ -9,6 +9,7 @@ #include <unordered_map> #include <memory> #include <string> +#include <ctime> #include <map> #define STREAM_NS "http://etherx.jabber.org/streams" @@ -25,6 +26,13 @@ #define VERSION_NS "jabber:iq:version" #define ADHOC_NS "http://jabber.org/protocol/commands" #define PING_NS "urn:xmpp:ping" +#define DELAY_NS "urn:xmpp:delay" +#define MAM_NS "urn:xmpp:mam:1" +#define FORWARD_NS "urn:xmpp:forward:0" +#define CLIENT_NS "jabber:client" +#define DATAFORM_NS "jabber:x:data" +#define RSM_NS "http://jabber.org/protocol/rsm" +#define MUC_TRAFFIC_NS "http://jabber.org/protocol/muc#traffic" /** * An XMPP component, communicating with an XMPP server using the protocole @@ -101,9 +109,8 @@ public: * If fulljid is false, the provided 'from' doesn't contain the * server-part of the JID and must be added. */ - void send_message(const std::string& from, Xmpp::body&& body, - const std::string& to, const std::string& type, - const bool fulljid=false); + void send_message(const std::string& from, Xmpp::body&& body, const std::string& to, + const std::string& type, const bool fulljid, const bool nocopy=false); /** * Send a join from a new participant */ @@ -121,12 +128,6 @@ public: const std::string& nick, const std::string& to); /** - * Send an error to indicate that the user tried to send a message to an - * invalid user. - */ - void send_invalid_user_error(const std::string& user_name, - const std::string& to); - /** * Send the MUC topic to the user */ void send_topic(const std::string& from, Xmpp::body&& xmpp_topic, const std::string& to, const std::string& who); @@ -135,6 +136,11 @@ public: */ void send_muc_message(const std::string& muc_name, const std::string& nick, Xmpp::body&& body, const std::string& jid_to); /** + * Send a message, with a <delay/> element, part of a MUC history + */ + void send_history_message(const std::string& muc_name, const std::string& nick, const std::string& body, + const std::string& jid_to, const std::time_t timestamp); + /** * Send an unavailable presence for this nick */ void send_muc_leave(const std::string& muc_name, std::string&& nick, Xmpp::body&& message, const std::string& jid_to, const bool self); @@ -151,11 +157,8 @@ public: /** * An user is kicked from a room */ - void kick_user(const std::string& muc_name, - const std::string& target, - const std::string& reason, - const std::string& author, - const std::string& jid_to); + void kick_user(const std::string& muc_name, const std::string& target, const std::string& reason, + const std::string& author, const std::string& jid_to, const bool self); /** * Send a generic presence error */ @@ -208,6 +211,9 @@ public: virtual void after_handshake() {} + const std::string& get_served_hostname() const + { return this->served_hostname; } + /** * Whether or not we ever succeeded our authentication to the XMPP server */ diff --git a/packaging/biboumi.spec.cmake b/packaging/biboumi.spec.cmake index c94da8b..0747d34 100644 --- a/packaging/biboumi.spec.cmake +++ b/packaging/biboumi.spec.cmake @@ -59,6 +59,9 @@ make check %{?_smp_mflags} %changelog +* Wed Nov 9 2016 Le Coz Florent <louiz@louiz.org> - 4.0-1 +- Update to 4.0 sources + * Thu Aug 4 2016 Le Coz Florent <louiz@louiz.org> - 3.0-1 - Update to 3.0 sources diff --git a/src/bridge/bridge.cpp b/src/bridge/bridge.cpp index 17d3ec6..a0ecc6e 100644 --- a/src/bridge/bridge.cpp +++ b/src/bridge/bridge.cpp @@ -1,5 +1,4 @@ #include <bridge/bridge.hpp> -#include <bridge/list_element.hpp> #include <xmpp/biboumi_component.hpp> #include <network/poller.hpp> #include <utils/empty_if_fixed_server.hpp> @@ -10,6 +9,8 @@ #include <utils/split.hpp> #include <xmpp/jid.hpp> #include <database/database.hpp> +#include "result_set_management.hpp" +#include <algorithm> using namespace std::string_literals; @@ -32,6 +33,10 @@ Bridge::Bridge(const std::string& user_jid, BiboumiComponent& xmpp, std::shared_ xmpp(xmpp), poller(poller) { +#ifdef USE_DATABASE + const auto options = Database::get_global_options(this->user_jid); + this->set_record_history(options.recordHistory.value()); +#endif } /** @@ -60,6 +65,20 @@ void Bridge::shutdown(const std::string& exit_message) } } +void Bridge::remove_resource(const std::string& resource, + const std::string& part_message) +{ + const auto resources_in_chan_copy = this->resources_in_chan; + for (const auto& chan_pair: resources_in_chan_copy) + { + const ChannelKey& channel_key = chan_pair.first; + const std::set<Resource>& resources = chan_pair.second; + if (resources.count(resource)) + this->leave_irc_channel({std::get<0>(channel_key), std::get<1>(channel_key), {}}, + part_message, resource); + } +} + void Bridge::clean() { auto it = this->irc_clients.begin(); @@ -133,7 +152,7 @@ IrcClient* Bridge::get_irc_client(const std::string& hostname) } } -IrcClient* Bridge::find_irc_client(const std::string& hostname) +IrcClient* Bridge::find_irc_client(const std::string& hostname) const { try { @@ -158,16 +177,23 @@ bool Bridge::join_irc_channel(const Iid& iid, const std::string& nickname, const { // Join the dummy channel if (irc->is_welcomed()) { - if (irc->get_dummy_channel().joined) + if (res_in_chan) return false; // Immediately simulate a message coming from the IRC server saying that we // joined the channel - const IrcMessage join_message(irc->get_nick(), "JOIN", {""}); - irc->on_channel_join(join_message); - const IrcMessage end_join_message(std::string(iid.get_server()), "366", - {irc->get_nick(), - "", "End of NAMES list"}); - irc->on_channel_completely_joined(end_join_message); + if (irc->get_dummy_channel().joined) + { + this->generate_channel_join_for_resource(iid, resource); + } + else + { + const IrcMessage join_message(irc->get_nick(), "JOIN", {""}); + irc->on_channel_join(join_message); + const IrcMessage end_join_message(std::string(iid.get_server()), "366", + {irc->get_nick(), + "", "End of NAMES list"}); + irc->on_channel_completely_joined(end_join_message); + } } else { @@ -224,6 +250,13 @@ void Bridge::send_channel_message(const Iid& iid, const std::string& body) irc->send_channel_message(iid.get_local(), action_prefix + line.substr(4) + "\01"); else irc->send_channel_message(iid.get_local(), line); + +#ifdef USE_DATABASE + const auto xmpp_body = this->make_xmpp_body(line); + if (this->record_history) + Database::store_muc_message(this->get_bare_jid(), iid, std::chrono::system_clock::now(), + std::get<0>(xmpp_body), irc->get_own_nick()); +#endif for (const auto& resource: this->resources_in_chan[iid.to_tuple()]) this->xmpp.send_muc_message(std::to_string(iid), irc->get_own_nick(), this->make_xmpp_body(line), this->user_jid + "/" + resource); @@ -324,7 +357,7 @@ void Bridge::send_raw_message(const std::string& hostname, const std::string& bo irc->send_raw(body); } -void Bridge::leave_irc_channel(Iid&& iid, std::string&& status_message, const std::string& resource) +void Bridge::leave_irc_channel(Iid&& iid, const std::string& status_message, const std::string& resource) { IrcClient* irc = this->get_irc_client(iid.get_server()); const auto key = iid.to_tuple(); @@ -334,7 +367,12 @@ void Bridge::leave_irc_channel(Iid&& iid, std::string&& status_message, const st const auto resources = this->number_of_resources_in_chan(key); if (resources == 1) { - irc->send_part_command(iid.get_local(), status_message); + // Do not send a PART message if we actually are not in that channel + // or if we already sent a PART but we are just waiting for the + // acknowledgment from the server + IrcChannel* channel = irc->get_channel(iid.get_local()); + if (channel->joined && !channel->parting) + irc->send_part_command(iid.get_local(), status_message); // Since there are no resources left in that channel, we don't // want to receive private messages using this room's JID this->remove_all_preferred_from_jid_of_room(iid.get_local()); @@ -361,45 +399,164 @@ void Bridge::send_irc_nick_change(const Iid& iid, const std::string& new_nick) irc->send_nick_command(new_nick); } -void Bridge::send_irc_channel_list_request(const Iid& iid, const std::string& iq_id, - const std::string& to_jid) +void Bridge::send_irc_channel_list_request(const Iid& iid, const std::string& iq_id, const std::string& to_jid, + ResultSetInfo rs_info) { - IrcClient* irc = this->get_irc_client(iid.get_server()); + auto& list = channel_list_cache[iid.get_server()]; + + // We fetch the list from the IRC server only if we have a complete + // cached list that needs to be invalidated (that is, when the request + // doesn’t have a after or before, or when the list is empty). + // If the list is not complete, this means that a request is already + // ongoing, so we just need to add the callback. + // By default the list is complete and empty. + if (list.complete && + (list.channels.empty() || (rs_info.after.empty() && rs_info.before.empty()))) + { + IrcClient* irc = this->get_irc_client(iid.get_server()); + irc->send_list_command(); + + // Add a callback that will populate our list + list.channels.clear(); + list.complete = false; + irc_responder_callback_t cb = [this, iid](const std::string& irc_hostname, + const IrcMessage& message) -> bool + { + if (irc_hostname != iid.get_server()) + return false; - irc->send_list_command(); + auto& list = channel_list_cache[iid.get_server()]; + + if (message.command == "263" || message.command == "RPL_TRYAGAIN" || message.command == "ERR_TOOMANYMATCHES" + || message.command == "ERR_NOSUCHSERVER") + { + list.complete = true; + return true; + } + else if (message.command == "322" || message.command == "RPL_LIST") + { // Add element to list + if (message.arguments.size() == 4) + { + list.channels.emplace_back(message.arguments[1] + utils::empty_if_fixed_server("%" + iid.get_server()), + message.arguments[2], message.arguments[3]); + } + return false; + } + else if (message.command == "323" || message.command == "RPL_LISTEND") + { // Send the iq response with the content of the list + list.complete = true; + return true; + } + return false; + }; - std::vector<ListElement> list; + this->add_waiting_irc(std::move(cb)); + } - irc_responder_callback_t cb = [this, iid, iq_id, to_jid, list=std::move(list)](const std::string& irc_hostname, - const IrcMessage& message) mutable -> bool + // If the list is complete, we immediately send the answer. + // Otherwise, we install a callback, that will populate our list and send + // the answer when we can. + if (list.complete) { - if (irc_hostname != iid.get_server()) + this->send_matching_channel_list(list, rs_info, iq_id, to_jid, std::to_string(iid)); + } + else + { + // Add a callback to answer the request as soon as we can + irc_responder_callback_t cb = [this, iid, iq_id, to_jid, + rs_info=std::move(rs_info)](const std::string& irc_hostname, + const IrcMessage& message) -> bool + { + if (irc_hostname != iid.get_server()) + return false; + + if (message.command == "263" || message.command == "RPL_TRYAGAIN" || message.command == "ERR_TOOMANYMATCHES" + || message.command == "ERR_NOSUCHSERVER") + { + std::string text; + if (message.arguments.size() >= 2) + text = message.arguments[1]; + this->xmpp.send_stanza_error("iq", to_jid, std::to_string(iid), iq_id, "wait", "service-unavailable", text, false); + return true; + } + else if (message.command == "322" || message.command == "RPL_LIST") + { + auto& list = channel_list_cache[iid.get_server()]; + const auto res = this->send_matching_channel_list(list, rs_info, iq_id, to_jid, std::to_string(iid)); + log_debug("We added a new channel in our list, can we send the result? ", std::boolalpha, res); + return res; + } + else if (message.command == "323" || message.command == "RPL_LISTEND") + { // Send the iq response with the content of the list + auto& list = channel_list_cache[iid.get_server()]; + this->send_matching_channel_list(list, rs_info, iq_id, to_jid, std::to_string(iid)); + return true; + } return false; - if (message.command == "263" || message.command == "RPL_TRYAGAIN" || - message.command == "ERR_TOOMANYMATCHES" || message.command == "ERR_NOSUCHSERVER") + }; + + this->add_waiting_irc(std::move(cb)); + } +} + +bool Bridge::send_matching_channel_list(const ChannelList& channel_list, const ResultSetInfo& rs_info, + const std::string& id, const std::string& to_jid, const std::string& from) +{ + auto begin = channel_list.channels.begin(); + auto end = channel_list.channels.begin(); + if (channel_list.complete) + { + begin = std::find_if(channel_list.channels.begin(), channel_list.channels.end(), [this, &rs_info](const ListElement& element) + { + return rs_info.after == element.channel + "@" + this->xmpp.get_served_hostname(); + }); + if (begin == channel_list.channels.end()) + begin = channel_list.channels.begin(); + else + begin = std::next(begin); + end = std::find_if(channel_list.channels.begin(), channel_list.channels.end(), [this, &rs_info](const ListElement& element) + { + return rs_info.before == element.channel + "@" + this->xmpp.get_served_hostname(); + }); + if (rs_info.max >= 0) { - std::string text; - if (message.arguments.size() >= 2) - text = message.arguments[1]; - this->xmpp.send_stanza_error("iq", to_jid, std::to_string(iid), iq_id, - "wait", "service-unavailable", text, false); - return true; + if (std::distance(begin, end) >= rs_info.max) + end = begin + rs_info.max; } - else if (message.command == "322" || message.command == "RPL_LIST") - { // Add element to list - if (message.arguments.size() == 4) - list.emplace_back(message.arguments[1], message.arguments[2], - message.arguments[3]); - return false; + } + else + { + if (rs_info.after.empty() && rs_info.before.empty() && rs_info.max < 0) + return false; + if (!rs_info.after.empty()) + { + begin = std::find_if(channel_list.channels.begin(), channel_list.channels.end(), [this, &rs_info](const ListElement& element) + { + return rs_info.after == element.channel + "@" + this->xmpp.get_served_hostname(); + }); + if (begin == channel_list.channels.end()) + return false; + begin = std::next(begin); } - else if (message.command == "323" || message.command == "RPL_LISTEND") - { // Send the iq response with the content of the list - this->xmpp.send_iq_room_list_result(iq_id, to_jid, std::to_string(iid), list); - return true; + if (!rs_info.before.empty()) + { + end = std::find_if(channel_list.channels.begin(), channel_list.channels.end(), [this, &rs_info](const ListElement& element) + { + return rs_info.before == element.channel + "@" + this->xmpp.get_served_hostname(); + }); + if (end == channel_list.channels.end()) + return false; } - return false; - }; - this->add_waiting_irc(std::move(cb)); + if (rs_info.max >= 0) + { + if (std::distance(begin, end) < rs_info.max) + return false; + else + end = begin + rs_info.max; + } + } + this->xmpp.send_iq_room_list_result(id, to_jid, from, channel_list, begin, end, rs_info); + return true; } void Bridge::send_irc_kick(const Iid& iid, const std::string& target, const std::string& reason, @@ -470,10 +627,11 @@ void Bridge::send_irc_user_ping_request(const std::string& irc_hostname, const s const std::string& iq_id, const std::string& to_jid, const std::string& from_jid) { - Iid iid(nick + "!" + irc_hostname); + Iid iid(nick, irc_hostname, Iid::Type::User); this->send_private_message(iid, "\01PING " + iq_id + "\01"); - irc_responder_callback_t cb = [this, nick=utils::tolower(nick), iq_id, to_jid, irc_hostname, from_jid](const std::string& hostname, const IrcMessage& message) -> bool + irc_responder_callback_t cb = [this, nick=utils::tolower(nick), iq_id, to_jid, irc_hostname, from_jid] + (const std::string& hostname, const IrcMessage& message) -> bool { if (irc_hostname != hostname || message.arguments.size() < 2) return false; @@ -537,20 +695,37 @@ void Bridge::on_gateway_ping(const std::string& irc_hostname, const std::string& "", true); } +void Bridge::send_irc_invitation(const Iid& iid, const std::string& to) +{ + IrcClient* irc = this->get_irc_client(iid.get_server()); + Jid to_jid(to); + std::string target_nick; + // Many ways to address a nick: + // A jid (ANY jid…) with a resource + if (!to_jid.resource.empty()) + target_nick = to_jid.resource; + else if (!to_jid.local.empty()) // A jid with a iid with a local part + target_nick = Iid(to_jid.local, {}).get_local(); + else + target_nick = to; // Not a jid, just the nick + irc->send_invitation(iid.get_local(), target_nick); +} + void Bridge::send_irc_version_request(const std::string& irc_hostname, const std::string& target, const std::string& iq_id, const std::string& to_jid, const std::string& from_jid) { - Iid iid(target + "!" + irc_hostname); + Iid iid(target, irc_hostname, Iid::Type::User); this->send_private_message(iid, "\01VERSION\01"); // TODO, add a timer to remove that waiting iq if the server does not // respond with a matching command before n seconds - irc_responder_callback_t cb = [this, target, iq_id, to_jid, irc_hostname, from_jid](const std::string& hostname, const IrcMessage& message) -> bool + irc_responder_callback_t cb = [this, target, iq_id, to_jid, irc_hostname, from_jid] + (const std::string& hostname, const IrcMessage& message) -> bool { if (irc_hostname != hostname) return false; IrcUser user(message.prefix); - if (message.command == "NOTICE" && user.nick == target && + if (message.command == "NOTICE" && utils::tolower(user.nick) == utils::tolower(target) && message.arguments.size() >= 2 && message.arguments[1].substr(0, 9) == "\01VERSION ") { // remove the "\01VERSION " and the "\01" parts from the string @@ -578,10 +753,17 @@ void Bridge::send_message(const Iid& iid, const std::string& nick, const std::st const auto encoding = in_encoding_for(*this, iid); if (muc) { +#ifdef USE_DATABASE + const auto xmpp_body = this->make_xmpp_body(body, encoding); + if (!nick.empty() && this->record_history) + Database::store_muc_message(this->get_bare_jid(), iid, std::chrono::system_clock::now(), + std::get<0>(xmpp_body), nick); +#endif for (const auto& resource: this->resources_in_chan[iid.to_tuple()]) { this->xmpp.send_muc_message(std::to_string(iid), nick, this->make_xmpp_body(body, encoding), this->user_jid + "/" + resource); + } } else @@ -590,16 +772,16 @@ void Bridge::send_message(const Iid& iid, const std::string& nick, const std::st const auto it = this->preferred_user_from.find(iid.get_local()); if (it != this->preferred_user_from.end()) { - const auto chan_name = Iid(Jid(it->second).local).get_local(); + const auto chan_name = Iid(Jid(it->second).local, {}).get_local(); for (const auto& resource: this->resources_in_chan[ChannelKey{chan_name, iid.get_server()}]) this->xmpp.send_message(it->second, this->make_xmpp_body(body, encoding), - this->user_jid + "/" + resource, "chat", true); + this->user_jid + "/" + resource, "chat", true, true); } else { for (const auto& resource: this->resources_in_server[iid.get_server()]) this->xmpp.send_message(std::to_string(iid), this->make_xmpp_body(body, encoding), - this->user_jid + "/" + resource, "chat", false); + this->user_jid + "/" + resource, "chat", false, true); } } } @@ -611,15 +793,16 @@ void Bridge::send_presence_error(const Iid& iid, const std::string& nick, this->xmpp.send_presence_error(std::to_string(iid), nick, this->user_jid, type, condition, error_code, text); } -void Bridge::send_muc_leave(Iid&& iid, std::string&& nick, const std::string& message, const bool self, const std::string& resource) +void Bridge::send_muc_leave(Iid&& iid, std::string&& nick, const std::string& message, const bool self, + const std::string& resource) { if (!resource.empty()) - this->xmpp.send_muc_leave(std::to_string(iid), std::move(nick), this->make_xmpp_body(message), this->user_jid + "/" + resource, - self); + this->xmpp.send_muc_leave(std::to_string(iid), std::move(nick), this->make_xmpp_body(message), + this->user_jid + "/" + resource, self); else for (const auto& res: this->resources_in_chan[iid.to_tuple()]) - this->xmpp.send_muc_leave(std::to_string(iid), std::move(nick), this->make_xmpp_body(message), this->user_jid + "/" + res, - self); + this->xmpp.send_muc_leave(std::to_string(iid), std::move(nick), this->make_xmpp_body(message), + this->user_jid + "/" + res, self); IrcClient* irc = this->find_irc_client(iid.get_server()); if (irc && irc->number_of_joined_channels() == 0) irc->send_quit_command(""); @@ -653,18 +836,29 @@ void Bridge::send_xmpp_message(const std::string& from, const std::string& autho else body = msg; - const auto encoding = in_encoding_for(*this, {from}); + const auto encoding = in_encoding_for(*this, {from, this}); for (const auto& resource: this->resources_in_server[from]) { - this->xmpp.send_message(from, this->make_xmpp_body(body, encoding), this->user_jid + "/" + resource, "chat"); + this->xmpp.send_message(from, this->make_xmpp_body(body, encoding), this->user_jid + "/" + resource, "chat", false, false); } } void Bridge::send_user_join(const std::string& hostname, const std::string& chan_name, const IrcUser* user, const char user_mode, const bool self) { - for (const auto& resource: this->resources_in_chan[ChannelKey{chan_name, hostname}]) - this->send_user_join(hostname, chan_name, user, user_mode, self, resource); + const auto resources = this->resources_in_chan[ChannelKey{chan_name, hostname}]; + if (self && resources.empty()) + { // This was a forced join: no client ever asked to join this room, + // but the server tells us we are in that room anyway. XMPP can’t + // do that, so we invite all the resources to join that channel. + const Iid iid(chan_name, hostname, Iid::Type::Channel); + this->send_xmpp_invitation(iid, ""); + } + else + { + for (const auto& resource: resources) + this->send_user_join(hostname, chan_name, user, user_mode, self, resource); + } } void Bridge::send_user_join(const std::string& hostname, const std::string& chan_name, @@ -682,7 +876,8 @@ void Bridge::send_user_join(const std::string& hostname, const std::string& chan affiliation, role, this->user_jid + "/" + resource, self); } -void Bridge::send_topic(const std::string& hostname, const std::string& chan_name, const std::string& topic, const std::string& who) +void Bridge::send_topic(const std::string& hostname, const std::string& chan_name, const std::string& topic, + const std::string& who) { for (const auto& resource: this->resources_in_chan[ChannelKey{chan_name, hostname}]) { @@ -696,18 +891,39 @@ void Bridge::send_topic(const std::string& hostname, const std::string& chan_nam { std::string encoded_chan_name(chan_name); xep0106::encode(encoded_chan_name); - const auto encoding = in_encoding_for(*this, {encoded_chan_name + '%' + hostname}); + const auto encoding = in_encoding_for(*this, {encoded_chan_name, hostname, Iid::Type::Channel}); this->xmpp.send_topic(encoded_chan_name + utils::empty_if_fixed_server( "%" + hostname), this->make_xmpp_body(topic, encoding), this->user_jid + "/" + resource, who); } +void Bridge::send_room_history(const std::string& hostname, const std::string& chan_name) +{ + for (const auto& resource: this->resources_in_chan[ChannelKey{chan_name, hostname}]) + this->send_room_history(hostname, chan_name, resource); +} + +void Bridge::send_room_history(const std::string& hostname, const std::string& chan_name, const std::string& resource) +{ +#ifdef USE_DATABASE + const auto coptions = Database::get_irc_channel_options_with_server_and_global_default(this->user_jid, hostname, chan_name); + const auto lines = Database::get_muc_logs(this->user_jid, chan_name, hostname, coptions.maxHistoryLength.value()); + for (const auto& line: lines) + { + const auto seconds = line.date.value().timeStamp(); + this->xmpp.send_history_message(chan_name + utils::empty_if_fixed_server("%" + hostname), line.nick.value(), + line.body.value(), + this->user_jid + "/" + resource, seconds); + } +#endif +} + std::string Bridge::get_own_nick(const Iid& iid) { IrcClient* irc = this->find_irc_client(iid.get_server()); if (irc) return irc->get_own_nick(); - return ""; + return {}; } size_t Bridge::active_clients() const @@ -715,16 +931,18 @@ size_t Bridge::active_clients() const return this->irc_clients.size(); } -void Bridge::kick_muc_user(Iid&& iid, const std::string& target, const std::string& reason, const std::string& author) +void Bridge::kick_muc_user(Iid&& iid, const std::string& target, const std::string& reason, const std::string& author, + const bool self) { for (const auto& resource: this->resources_in_chan[iid.to_tuple()]) - this->xmpp.kick_user(std::to_string(iid), target, reason, author, this->user_jid + "/" + resource); + this->xmpp.kick_user(std::to_string(iid), target, reason, author, this->user_jid + "/" + resource, self); } void Bridge::send_nickname_conflict_error(const Iid& iid, const std::string& nickname) { - for (const auto& resource: this->resources_in_chan[iid.to_tuple()]) - this->xmpp.send_presence_error(std::to_string(iid), nickname, this->user_jid + "/" + resource, "cancel", "conflict", "409", ""); + for (const auto& resource: this->resources_in_chan[iid.to_tuple()]) + this->xmpp.send_presence_error(std::to_string(iid), nickname, this->user_jid + "/" + resource, + "cancel", "conflict", "409", ""); } void Bridge::send_affiliation_role_change(const Iid& iid, const std::string& target, const char mode) @@ -734,14 +952,16 @@ void Bridge::send_affiliation_role_change(const Iid& iid, const std::string& tar std::tie(role, affiliation) = get_role_affiliation_from_irc_mode(mode); for (const auto& resource: this->resources_in_chan[iid.to_tuple()]) - this->xmpp.send_affiliation_role_change(std::to_string(iid), target, affiliation, role, this->user_jid + "/" + resource); + this->xmpp.send_affiliation_role_change(std::to_string(iid), target, affiliation, role, + this->user_jid + "/" + resource); } void Bridge::send_iq_version_request(const std::string& nick, const std::string& hostname) { const auto resources = this->resources_in_server[hostname]; if (resources.begin() != resources.end()) - this->xmpp.send_iq_version_request(utils::tolower(nick) + "!" + utils::empty_if_fixed_server(hostname), this->user_jid + "/" + *resources.begin()); + this->xmpp.send_iq_version_request(utils::tolower(nick) + "%" + utils::empty_if_fixed_server(hostname), + this->user_jid + "/" + *resources.begin()); } void Bridge::send_xmpp_ping_request(const std::string& nick, const std::string& hostname, @@ -753,7 +973,14 @@ void Bridge::send_xmpp_ping_request(const std::string& nick, const std::string& // Forward to the first resource (arbitrary, based on the “order” of the std::set) only const auto resources = this->resources_in_server[hostname]; if (resources.begin() != resources.end()) - this->xmpp.send_ping_request(utils::tolower(nick) + "!" + utils::empty_if_fixed_server(hostname), this->user_jid + "/" + *resources.begin(), utils::revstr(id)); + this->xmpp.send_ping_request(utils::tolower(nick) + "%" + utils::empty_if_fixed_server(hostname), + this->user_jid + "/" + *resources.begin(), utils::revstr(id)); +} + +void Bridge::send_xmpp_invitation(const Iid& iid, const std::string& author) +{ + for (const auto& resource: this->resources_in_server[iid.get_server()]) + this->xmpp.send_invitation(std::to_string(iid), this->user_jid + "/" + resource, author); } void Bridge::set_preferred_from_jid(const std::string& nick, const std::string& full_jid) @@ -776,7 +1003,7 @@ void Bridge::remove_all_preferred_from_jid_of_room(const std::string& channel_na { for (auto it = this->preferred_user_from.begin(); it != this->preferred_user_from.end();) { - Iid iid(Jid(it->second).local); + Iid iid(Jid(it->second).local, {}); if (iid.get_local() == channel_name) it = this->preferred_user_from.erase(it); else @@ -806,6 +1033,14 @@ std::unordered_map<std::string, std::shared_ptr<IrcClient>>& Bridge::get_irc_cli return this->irc_clients; } +std::set<char> Bridge::get_chantypes(const std::string& hostname) const +{ + IrcClient* irc = this->find_irc_client(hostname); + if (!irc) + return {'#', '&'}; + return irc->get_chantypes(); +} + void Bridge::add_resource_to_chan(const Bridge::ChannelKey& channel, const std::string& resource) { auto it = this->resources_in_chan.find(channel); @@ -894,7 +1129,6 @@ void Bridge::generate_channel_join_for_resource(const Iid& iid, const std::strin { if (user->nick != self->nick) { - log_debug(user->nick); this->send_user_join(iid.get_server(), iid.get_encoded_local(), user.get(), user->get_most_significant_mode(irc->get_sorted_user_modes()), false, resource); @@ -903,5 +1137,13 @@ void Bridge::generate_channel_join_for_resource(const Iid& iid, const std::strin this->send_user_join(iid.get_server(), iid.get_encoded_local(), self, self->get_most_significant_mode(irc->get_sorted_user_modes()), true, resource); + this->send_room_history(iid.get_server(), iid.get_local(), resource); this->send_topic(iid.get_server(), iid.get_encoded_local(), channel->topic, channel->topic_author, resource); } + +#ifdef USE_DATABASE +void Bridge::set_record_history(const bool val) +{ + this->record_history = val; +} +#endif diff --git a/src/bridge/bridge.hpp b/src/bridge/bridge.hpp index 69b7bd5..18ebfeb 100644 --- a/src/bridge/bridge.hpp +++ b/src/bridge/bridge.hpp @@ -1,5 +1,7 @@ #pragma once +#include <bridge/result_set_management.hpp> +#include <bridge/list_element.hpp> #include <irc/irc_message.hpp> #include <irc/irc_client.hpp> @@ -13,8 +15,11 @@ #include <string> #include <memory> +#include <biboumi.h> + class BiboumiComponent; class Poller; +struct ResultSetInfo; /** * A callback called for each IrcMessage we receive. If the message triggers @@ -45,6 +50,10 @@ public: */ void shutdown(const std::string& exit_message); /** + * PART the given resource from all the channels + */ + void remove_resource(const std::string& resource, const std::string& part_message); + /** * Remove all inactive IrcClients */ void clean(); @@ -70,7 +79,7 @@ public: void send_channel_message(const Iid& iid, const std::string& body); void send_private_message(const Iid& iid, const std::string& body, const std::string& type="PRIVMSG"); void send_raw_message(const std::string& hostname, const std::string& body); - void leave_irc_channel(Iid&& iid, std::string&& status_message, const std::string& resource); + void leave_irc_channel(Iid&& iid, const std::string& status_message, const std::string& resource); void send_irc_nick_change(const Iid& iid, const std::string& new_nick); void send_irc_kick(const Iid& iid, const std::string& target, const std::string& reason, const std::string& iq_id, const std::string& to_jid); @@ -81,8 +90,19 @@ public: void send_irc_version_request(const std::string& irc_hostname, const std::string& target, const std::string& iq_id, const std::string& to_jid, const std::string& from_jid); - void send_irc_channel_list_request(const Iid& iid, const std::string& iq_id, - const std::string& to_jid); + void send_irc_channel_list_request(const Iid& iid, const std::string& iq_id, const std::string& to_jid, + ResultSetInfo rs_info); + /** + * Check if the channel list contains what is needed to answer the RSM request, + * if it does, send the iq result. If the list is complete but does not contain + * everything, send the result anyway (because there are no more available + * channels that could complete the list). + * + * Returns true if we sent the answer. + */ + bool send_matching_channel_list(const ChannelList& channel_list, + const ResultSetInfo& rs_info, const std::string& id, const std::string& to_jid, + const std::string& from); void forward_affiliation_role_change(const Iid& iid, const std::string& nick, const std::string& affiliation, const std::string& role); /** @@ -105,6 +125,8 @@ public: void on_gateway_ping(const std::string& irc_hostname, const std::string& iq_id, const std::string& to_jid, const std::string& from_jid); + void send_irc_invitation(const Iid& iid, const std::string& to); + /*** ** ** From IRC to XMPP. @@ -132,6 +154,11 @@ public: void send_topic(const std::string& hostname, const std::string& chan_name, const std::string& topic, const std::string& who); void send_topic(const std::string& hostname, const std::string& chan_name, const std::string& topic, const std::string& who, const std::string& resource); /** + * Send the MUC history to the user + */ + void send_room_history(const std::string& hostname, const std::string& chan_name); + void send_room_history(const std::string& hostname, const std::string& chan_name, const std::string& resource); + /** * Send a MUC message from some participant */ void send_message(const Iid& iid, const std::string& nick, const std::string& body, const bool muc); @@ -154,7 +181,8 @@ public: const std::string& new_nick, const char user_mode, const bool self); - void kick_muc_user(Iid&& iid, const std::string& target, const std::string& reason, const std::string& author); + void kick_muc_user(Iid&& iid, const std::string& target, const std::string& reason, const std::string& author, + const bool self); void send_nickname_conflict_error(const Iid& iid, const std::string& nickname); /** * Send a role/affiliation change, matching the change of mode for that user @@ -169,6 +197,8 @@ public: */ void send_xmpp_ping_request(const std::string& nick, const std::string& hostname, const std::string& id); + void send_xmpp_invitation(const Iid& iid, const std::string& author); + /** * Misc */ @@ -201,6 +231,10 @@ public: */ 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(); + std::set<char> get_chantypes(const std::string& hostname) const; +#ifdef USE_DATABASE + void set_record_history(const bool val); +#endif private: /** @@ -214,10 +248,12 @@ private: * a IRCServerNotConnected error in that case. */ IrcClient* get_irc_client(const std::string& hostname); +public: /** * Idem, but returns nullptr if the server does not exist. */ - IrcClient* find_irc_client(const std::string& hostname); + IrcClient* find_irc_client(const std::string& hostname) const; +private: /** * The bare JID of the user associated with this bridge. Messages from/to this * JID are only managed by this bridge. @@ -252,7 +288,6 @@ private: * response iq. */ std::vector<irc_responder_callback_t> waiting_irc; - /** * Resources to IRC channel/server mapping: */ @@ -260,7 +295,9 @@ private: using ChannelName = std::string; using IrcHostname = std::string; using ChannelKey = std::tuple<ChannelName, IrcHostname>; +public: std::map<ChannelKey, std::set<Resource>> resources_in_chan; +private: std::map<IrcHostname, std::set<Resource>> resources_in_server; /** * Manage which resource is in which channel @@ -281,6 +318,16 @@ private: * TODO: send message history */ void generate_channel_join_for_resource(const Iid& iid, const std::string& resource); + /** + * A cache of the channels list (as returned by the server on a LIST + * request), to be re-used on a subsequent XMPP list request that + * uses result-set-management. + */ + std::map<IrcHostname, ChannelList> channel_list_cache; + +#ifdef USE_DATABASE + bool record_history { true }; +#endif }; struct IRCNotConnected: public std::exception diff --git a/src/bridge/list_element.hpp b/src/bridge/list_element.hpp index 1eff2ee..554c83d 100644 --- a/src/bridge/list_element.hpp +++ b/src/bridge/list_element.hpp @@ -1,6 +1,6 @@ #pragma once - +#include <vector> #include <string> struct ListElement @@ -17,3 +17,8 @@ struct ListElement }; +struct ChannelList +{ + bool complete{true}; + std::vector<ListElement> channels{}; +}; diff --git a/src/bridge/result_set_management.hpp b/src/bridge/result_set_management.hpp new file mode 100644 index 0000000..6ff82ba --- /dev/null +++ b/src/bridge/result_set_management.hpp @@ -0,0 +1,10 @@ +#pragma once + +#include <string> + +struct ResultSetInfo +{ + int max{-1}; + std::string before{}; + std::string after{}; +}; diff --git a/src/database/database.cpp b/src/database/database.cpp index 61e1b47..f7d309b 100644 --- a/src/database/database.cpp +++ b/src/database/database.cpp @@ -3,7 +3,10 @@ #include <database/database.hpp> #include <logger/logger.hpp> -#include <string> +#include <irc/iid.hpp> +#include <uuid/uuid.h> +#include <utils/get_first_non_empty.hpp> +#include <utils/time.hpp> using namespace std::string_literals; @@ -29,6 +32,19 @@ void Database::set_verbose(const bool val) Database::db->verbose = val; } +db::GlobalOptions Database::get_global_options(const std::string& owner) +{ + try { + auto options = litesql::select<db::GlobalOptions>(*Database::db, + db::GlobalOptions::Owner == owner).one(); + return options; + } catch (const litesql::NotFound& e) { + db::GlobalOptions options(*Database::db); + options.owner = owner; + return options; + } +} + db::IrcServerOptions Database::get_irc_server_options(const std::string& owner, const std::string& server) { @@ -71,17 +87,96 @@ db::IrcChannelOptions Database::get_irc_channel_options_with_server_default(cons { auto coptions = Database::get_irc_channel_options(owner, server, channel); auto soptions = Database::get_irc_server_options(owner, server); - if (coptions.encodingIn.value().empty()) - coptions.encodingIn = soptions.encodingIn; - if (coptions.encodingOut.value().empty()) - coptions.encodingOut = soptions.encodingOut; + + coptions.encodingIn = get_first_non_empty(coptions.encodingIn.value(), + soptions.encodingIn.value()); + coptions.encodingOut = get_first_non_empty(coptions.encodingOut.value(), + soptions.encodingOut.value()); + + coptions.maxHistoryLength = get_first_non_empty(coptions.maxHistoryLength.value(), + soptions.maxHistoryLength.value()); return coptions; } +db::IrcChannelOptions Database::get_irc_channel_options_with_server_and_global_default(const std::string& owner, + const std::string& server, + const std::string& channel) +{ + auto coptions = Database::get_irc_channel_options(owner, server, channel); + auto soptions = Database::get_irc_server_options(owner, server); + auto goptions = Database::get_global_options(owner); + + coptions.encodingIn = get_first_non_empty(coptions.encodingIn.value(), + soptions.encodingIn.value()); + coptions.encodingOut = get_first_non_empty(coptions.encodingOut.value(), + soptions.encodingOut.value()); + + coptions.maxHistoryLength = get_first_non_empty(coptions.maxHistoryLength.value(), + soptions.maxHistoryLength.value(), + goptions.maxHistoryLength.value()); + + return coptions; +} + +void Database::store_muc_message(const std::string& owner, const Iid& iid, + Database::time_point date, + const std::string& body, + const std::string& nick) +{ + db::MucLogLine line(*Database::db); + + line.uuid = Database::gen_uuid(); + line.owner = owner; + line.ircChanName = iid.get_local(); + line.ircServerName = iid.get_server(); + line.date = date.time_since_epoch().count() / 1'000'000'000; + line.body = body; + line.nick = nick; + + line.update(); +} + +std::vector<db::MucLogLine> Database::get_muc_logs(const std::string& owner, const std::string& chan_name, const std::string& server, + int limit, const std::string& start, const std::string& end) +{ + auto request = litesql::select<db::MucLogLine>(*Database::db, + db::MucLogLine::Owner == owner && + db::MucLogLine::IrcChanName == chan_name && + db::MucLogLine::IrcServerName == server); + request.orderBy(db::MucLogLine::Id, false); + + if (limit >= 0) + request.limit(limit); + if (!start.empty()) + { + const auto start_time = utils::parse_datetime(start); + if (start_time != -1) + request.where(db::MucLogLine::Date >= start_time); + } + if (!end.empty()) + { + const auto end_time = utils::parse_datetime(end); + if (end_time != -1) + request.where(db::MucLogLine::Date <= end_time); + } + const auto& res = request.all(); + return {res.crbegin(), res.crend()}; +} + void Database::close() { Database::db.reset(nullptr); } +std::string Database::gen_uuid() +{ + char uuid_str[37]; + uuid_t uuid; + uuid_generate(uuid); + uuid_unparse(uuid, uuid_str); + return uuid_str; +} + + #endif diff --git a/src/database/database.hpp b/src/database/database.hpp index 7173bcd..6823574 100644 --- a/src/database/database.hpp +++ b/src/database/database.hpp @@ -9,10 +9,14 @@ #include <memory> #include <litesql.hpp> +#include <chrono> + +class Iid; class Database { public: + using time_point = std::chrono::system_clock::time_point; Database() = default; ~Database() = default; @@ -32,6 +36,7 @@ public: * Return the object from the db. Create it beforehand (with all default * values) if it is not already present. */ + static db::GlobalOptions get_global_options(const std::string& owner); static db::IrcServerOptions get_irc_server_options(const std::string& owner, const std::string& server); static db::IrcChannelOptions get_irc_channel_options(const std::string& owner, @@ -40,12 +45,20 @@ public: static db::IrcChannelOptions get_irc_channel_options_with_server_default(const std::string& owner, const std::string& server, const std::string& channel); + static db::IrcChannelOptions get_irc_channel_options_with_server_and_global_default(const std::string& owner, + const std::string& server, + const std::string& channel); + static std::vector<db::MucLogLine> get_muc_logs(const std::string& owner, const std::string& chan_name, const std::string& server, + int limit=-1, const std::string& before="", const std::string& after=""); + static void store_muc_message(const std::string& owner, const Iid& iid, + time_point date, const std::string& body, const std::string& nick); static void close(); static void open(const std::string& filename, const std::string& db_type="sqlite3"); private: + static std::string gen_uuid(); static std::unique_ptr<db::BibouDB> db; }; #endif /* USE_DATABASE */ diff --git a/src/irc/iid.cpp b/src/irc/iid.cpp index 0e2841e..d442013 100644 --- a/src/irc/iid.cpp +++ b/src/irc/iid.cpp @@ -1,62 +1,72 @@ #include <utils/tolower.hpp> #include <config/config.hpp> - +#include <bridge/bridge.hpp> #include <irc/iid.hpp> #include <utils/encoding.hpp> -Iid::Iid(const std::string& iid): - is_channel(false), - is_user(false) +constexpr char Iid::separator[]; + +Iid::Iid(const std::string& local, const std::string& server, Iid::Type type): + type(type), + local(local), + server(server) { - const std::string fixed_irc_server = Config::get("fixed_irc_server", ""); - if (fixed_irc_server.empty()) - this->init(iid); - else - this->init_with_fixed_server(iid, fixed_irc_server); } +Iid::Iid(const std::string& iid, const std::set<char>& chantypes) +{ + this->init(iid); + this->set_type(std::set<char>(chantypes)); +} -void Iid::init(const std::string& iid) +Iid::Iid(const std::string& iid, const std::initializer_list<char>& chantypes): + Iid(iid, std::set<char>(chantypes)) { - const std::string::size_type sep = iid.find_first_of("%!"); - if (sep != std::string::npos) - { - if (iid[sep] == '%') - this->is_channel = true; - else - this->is_user = true; - this->set_local(iid.substr(0, sep)); - this->set_server(iid.substr(sep + 1)); - } - else - this->set_server(iid); } -void Iid::init_with_fixed_server(const std::string& iid, const std::string& hostname) +Iid::Iid(const std::string& iid, const Bridge *bridge) +{ + this->init(iid); + const auto chantypes = bridge->get_chantypes(this->server); + this->set_type(chantypes); +} + +void Iid::set_type(const std::set<char>& chantypes) { - this->set_server(hostname); + if (this->local.empty()) + return; - const std::string::size_type sep = iid.find("!"); + if (chantypes.count(this->local[0]) == 1) + this->type = Iid::Type::Channel; + else + this->type = Iid::Type::User; +} + +void Iid::init(const std::string& iid) +{ + const std::string fixed_irc_server = Config::get("fixed_irc_server", ""); - // Without any separator, we consider that it's a channel - if (sep == std::string::npos) + if (fixed_irc_server.empty()) + { + const std::string::size_type sep = iid.find('%'); + if (sep != std::string::npos) { - this->is_channel = true; - this->set_local(iid); + this->set_local(iid.substr(0, sep)); + this->set_server(iid.substr(sep + 1)); + this->type = Iid::Type::Channel; } - else // A separator can be present to differenciate a channel from a user, - // but the part behind it (the hostname) is ignored + else { - this->set_local(iid.substr(0, sep)); - this->is_user = true; + this->set_server(iid); + this->type = Iid::Type::Server; } -} - -Iid::Iid(): - is_channel(false), - is_user(false) -{ + } + else + { + this->set_server(fixed_irc_server); + this->set_local(iid); + } } void Iid::set_local(const std::string& loc) @@ -88,27 +98,18 @@ const std::string& Iid::get_server() const return this->server; } -std::string Iid::get_sep() const -{ - if (this->is_channel) - return "%"; - else if (this->is_user) - return "!"; - return ""; -} - namespace std { const std::string to_string(const Iid& iid) { if (Config::get("fixed_irc_server", "").empty()) - return iid.get_encoded_local() + iid.get_sep() + iid.get_server(); + { + if (iid.type == Iid::Type::Server) + return iid.get_server(); + else + return iid.get_encoded_local() + iid.separator + iid.get_server(); + } else - { - if (iid.get_sep() == "!") - return iid.get_encoded_local() + iid.get_sep(); - else - return iid.get_encoded_local(); - } + return iid.get_encoded_local(); } } diff --git a/src/irc/iid.hpp b/src/irc/iid.hpp index 3b11470..44861c1 100644 --- a/src/irc/iid.hpp +++ b/src/irc/iid.hpp @@ -2,48 +2,64 @@ #include <string> +#include <set> + +class Bridge; /** * A name representing an IRC channel on an IRC server, or an IRC user on an * IRC server, or just an IRC server. * - * The separator for an user is '!', for a channel it's '%'. If no separator - * is present, it's just an irc server. + * The separator is '%' between the local part (nickname or channel) and the + * server part. If no separator is present, it's just an irc server. + * If it is present, the first character of the local part determines if it’s + * a channel or a user: ff the local part is empty or if its first character + * is part of the chantypes characters, then it’s a channel, otherwise it’s + * a user. + * * It’s possible to have an empty-string server, but it makes no sense in - * the biboumi context. + * biboumi’s context. + * + * Assuming the chantypes are '#' and '&': * * #test%irc.example.org has : * - local: "#test" (the # is part of the name, it could very well be absent, or & (for example) instead) * - server: "irc.example.org" - * - is_channel: true - * - is_user: false + * - type: channel * * %irc.example.org: * - local: "" * - server: "irc.example.org" - * - is_channel: true - * - is_user: false - * Note: this is the special empty-string channel, used internal in biboumi + * - type: channel + * Note: this is the special empty-string channel, used internally in biboumi * but has no meaning on IRC. * - * foo!irc.example.org + * foo%irc.example.org * - local: "foo" * - server: "irc.example.org" - * - is_channel: false - * - is_user: true - * Note: the empty-string user (!irc.example.org) has no special meaning in biboumi + * - type: user + * Note: the empty-string user (!irc.example.org) makes no sense for biboumi * * irc.example.org: * - local: "" * - server: "irc.example.org" - * - is_channel: false - * - is_user: false + * - type: server */ class Iid { public: - Iid(const std::string& iid); - Iid(); + enum class Type + { + Channel, + User, + Server, + }; + static constexpr char separator[]{"%"}; + Iid(const std::string& iid, const std::set<char>& chantypes); + Iid(const std::string& iid, const std::initializer_list<char>& chantypes); + Iid(const std::string& iid, const Bridge* bridge); + Iid(const std::string& local, const std::string& server, Type type); + Iid() = default; Iid(const Iid&) = default; Iid(Iid&&) = delete; @@ -52,21 +68,19 @@ public: void set_local(const std::string& loc); void set_server(const std::string& serv); + const std::string& get_local() const; const std::string get_encoded_local() const; const std::string& get_server() const; - bool is_channel; - bool is_user; - - std::string get_sep() const; - std::tuple<std::string, std::string> to_tuple() const; + Type type { Type::Server }; + private: void init(const std::string& iid); - void init_with_fixed_server(const std::string& iid, const std::string& hostname); + void set_type(const std::set<char>& chantypes); std::string local; std::string server; diff --git a/src/irc/irc_channel.cpp b/src/irc/irc_channel.cpp index e769245..40d7f54 100644 --- a/src/irc/irc_channel.cpp +++ b/src/irc/irc_channel.cpp @@ -1,12 +1,6 @@ #include <irc/irc_channel.hpp> #include <algorithm> -IrcChannel::IrcChannel(): - joined(false), - self(nullptr) -{ -} - void IrcChannel::set_self(const std::string& name) { this->self = std::make_unique<IrcUser>(name); diff --git a/src/irc/irc_channel.hpp b/src/irc/irc_channel.hpp index 2bcefaf..7c269b9 100644 --- a/src/irc/irc_channel.hpp +++ b/src/irc/irc_channel.hpp @@ -14,16 +14,19 @@ class IrcChannel { public: - explicit IrcChannel(); + IrcChannel() = default; IrcChannel(const IrcChannel&) = delete; IrcChannel(IrcChannel&&) = delete; IrcChannel& operator=(const IrcChannel&) = delete; IrcChannel& operator=(IrcChannel&&) = delete; - bool joined; - std::string topic; - std::string topic_author; + bool joined{false}; + // Set to true if we sent a PART but didn’t yet receive the PART ack from + // the server + bool parting{false}; + std::string topic{}; + std::string topic_author{}; void set_self(const std::string& name); IrcUser* get_self() const; IrcUser* add_user(const std::string& name, @@ -35,8 +38,8 @@ public: { return this->users; } protected: - std::unique_ptr<IrcUser> self; - std::vector<std::unique_ptr<IrcUser>> users; + std::unique_ptr<IrcUser> self{}; + std::vector<std::unique_ptr<IrcUser>> users{}; }; /** diff --git a/src/irc/irc_client.cpp b/src/irc/irc_client.cpp index dd83307..b0d3a47 100644 --- a/src/irc/irc_client.cpp +++ b/src/irc/irc_client.cpp @@ -46,6 +46,7 @@ static const std::unordered_map<std::string, {"323", {&IrcClient::on_rpl_listend, {0, 0}}}, {"RPL_NOTOPIC", {&IrcClient::on_empty_topic, {0, 0}}}, {"331", {&IrcClient::on_empty_topic, {0, 0}}}, + {"341", {&IrcClient::on_invited, {3, 0}}}, {"RPL_MOTDSTART", {&IrcClient::empty_motd, {0, 0}}}, {"375", {&IrcClient::empty_motd, {0, 0}}}, {"RPL_MOTD", {&IrcClient::on_motd_line, {2, 0}}}, @@ -64,6 +65,8 @@ static const std::unordered_map<std::string, {"432", {&IrcClient::on_erroneous_nickname, {2, 0}}}, {"433", {&IrcClient::on_nickname_conflict, {2, 0}}}, {"438", {&IrcClient::on_nickname_change_too_fast, {2, 0}}}, + {"443", {&IrcClient::on_useronchannel, {3, 0}}}, + {"ERR_USERONCHANNEL", {&IrcClient::on_useronchannel, {3, 0}}}, {"001", {&IrcClient::on_welcome_message, {1, 0}}}, {"PART", {&IrcClient::on_part, {1, 0}}}, {"ERROR", {&IrcClient::on_error, {1, 0}}}, @@ -73,6 +76,7 @@ static const std::unordered_map<std::string, {"PING", {&IrcClient::send_pong_command, {1, 0}}}, {"PONG", {&IrcClient::on_pong, {0, 0}}}, {"KICK", {&IrcClient::on_kick, {3, 0}}}, + {"INVITE", {&IrcClient::on_invite, {2, 0}}}, {"401", {&IrcClient::on_generic_error, {2, 0}}}, {"402", {&IrcClient::on_generic_error, {2, 0}}}, @@ -95,7 +99,6 @@ static const std::unordered_map<std::string, {"436", {&IrcClient::on_generic_error, {2, 0}}}, {"441", {&IrcClient::on_generic_error, {2, 0}}}, {"442", {&IrcClient::on_generic_error, {2, 0}}}, - {"443", {&IrcClient::on_generic_error, {2, 0}}}, {"444", {&IrcClient::on_generic_error, {2, 0}}}, {"446", {&IrcClient::on_generic_error, {2, 0}}}, {"451", {&IrcClient::on_generic_error, {2, 0}}}, @@ -213,7 +216,7 @@ void IrcClient::on_connection_failed(const std::string& reason) // Send an error message for all room that the user wanted to join for (const auto& tuple: this->channels_to_join) { - Iid iid(std::get<0>(tuple) + "%" + this->hostname); + Iid iid(std::get<0>(tuple) + "%" + this->hostname, this->chantypes); this->bridge.send_presence_error(iid, this->current_nick, "cancel", "item-not-found", "", reason); @@ -426,7 +429,12 @@ void IrcClient::send_kick_command(const std::string& chan_name, const std::strin void IrcClient::send_list_command() { - this->send_message(IrcMessage("LIST", {})); + this->send_message(IrcMessage("LIST", {"*"})); +} + +void IrcClient::send_invitation(const std::string& chan_name, const std::string& nick) +{ + this->send_message(IrcMessage("INVITE", {nick, chan_name})); } void IrcClient::send_topic_command(const std::string& chan_name, const std::string& topic) @@ -495,6 +503,7 @@ void IrcClient::send_part_command(const std::string& chan_name, const std::strin this->leave_dummy_channel(status_message); else this->send_message(IrcMessage("PART", {chan_name, status_message})); + channel->parting = true; } } @@ -551,7 +560,7 @@ void IrcClient::on_notice(const IrcMessage& message) if (this->nicks_to_treat_as_private.find(nick) != this->nicks_to_treat_as_private.end()) { // We previously sent a message to that nick) - this->bridge.send_message({nick + "!" + this->hostname}, nick, body, + this->bridge.send_message({nick, this->hostname, Iid::Type::User}, nick, body, false); } else @@ -663,12 +672,12 @@ void IrcClient::on_channel_message(const IrcMessage& message) bool muc = true; if (!this->get_channel(iid.get_local())->joined) { - iid.is_user = true; + iid.type = Iid::Type::User; iid.set_local(nick); muc = false; } else - iid.is_channel = true; + iid.type = Iid::Type::Channel; if (!body.empty() && body[0] == '\01') { if (body.substr(1, 6) == "ACTION") @@ -701,6 +710,14 @@ void IrcClient::empty_motd(const IrcMessage&) this->motd.erase(); } +void IrcClient::on_invited(const IrcMessage& message) +{ + const std::string& chan_name = message.arguments[2]; + const std::string& invited_nick = message.arguments[1]; + + this->bridge.send_xmpp_message(this->hostname, "", invited_nick + " has been invited to " + chan_name); +} + void IrcClient::on_empty_topic(const IrcMessage& message) { const std::string chan_name = utils::tolower(message.arguments[1]); @@ -748,7 +765,9 @@ void IrcClient::on_channel_completely_joined(const IrcMessage& message) const std::string chan_name = utils::tolower(message.arguments[1]); IrcChannel* channel = this->get_channel(chan_name); channel->joined = true; - this->bridge.send_user_join(this->hostname, chan_name, channel->get_self(), channel->get_self()->get_most_significant_mode(this->sorted_user_modes), true); + this->bridge.send_user_join(this->hostname, chan_name, channel->get_self(), + channel->get_self()->get_most_significant_mode(this->sorted_user_modes), true); + this->bridge.send_room_history(this->hostname, chan_name); this->bridge.send_topic(this->hostname, chan_name, channel->topic, channel->topic_author); } @@ -780,7 +799,7 @@ void IrcClient::on_nickname_conflict(const IrcMessage& message) Iid iid; iid.set_local(it->first); iid.set_server(this->hostname); - iid.is_channel = true; + iid.type = Iid::Type::Channel; this->bridge.send_nickname_conflict_error(iid, nickname); } } @@ -797,13 +816,12 @@ void IrcClient::on_nickname_change_too_fast(const IrcMessage& message) Iid iid; iid.set_local(it->first); iid.set_server(this->hostname); - iid.is_channel = true; + iid.type = Iid::Type::Channel; this->bridge.send_presence_error(iid, nickname, "cancel", "not-acceptable", "", txt); } } - void IrcClient::on_generic_error(const IrcMessage& message) { const std::string error_msg = message.arguments.size() >= 3 ? @@ -811,6 +829,12 @@ void IrcClient::on_generic_error(const IrcMessage& message) this->send_gateway_message(message.arguments[1] + ": " + error_msg, message.prefix); } +void IrcClient::on_useronchannel(const IrcMessage& message) +{ + this->send_gateway_message(message.arguments[1] + " " + message.arguments[3] + " " + + message.arguments[2]); +} + void IrcClient::on_welcome_message(const IrcMessage& message) { this->current_nick = message.arguments[0]; @@ -858,7 +882,7 @@ void IrcClient::on_part(const IrcMessage& message) Iid iid; iid.set_local(chan_name); iid.set_server(this->hostname); - iid.is_channel = true; + iid.type = Iid::Type::Channel; bool self = channel->get_self()->nick == nick; if (self) { @@ -880,7 +904,7 @@ void IrcClient::on_error(const IrcMessage& message) Iid iid; iid.set_local(it->first); iid.set_server(this->hostname); - iid.is_channel = true; + iid.type = Iid::Type::Channel; IrcChannel* channel = it->second.get(); if (!channel->joined) continue; @@ -908,7 +932,7 @@ void IrcClient::on_quit(const IrcMessage& message) Iid iid; iid.set_local(chan_name); iid.set_server(this->hostname); - iid.is_channel = true; + iid.type = Iid::Type::Channel; this->bridge.send_muc_leave(std::move(iid), std::move(nick), txt, false); } } @@ -916,29 +940,38 @@ void IrcClient::on_quit(const IrcMessage& message) void IrcClient::on_nick(const IrcMessage& message) { - const std::string new_nick = message.arguments[0]; + const std::string new_nick = IrcUser(message.arguments[0]).nick; + const std::string current_nick = IrcUser(message.prefix).nick; + const auto change_nick_func = [this, &new_nick, ¤t_nick](const std::string& chan_name, const IrcChannel* channel) + { + IrcUser* user; + if (channel->get_self() && channel->get_self()->nick == current_nick) + user = channel->get_self(); + else + user = channel->find_user(current_nick); + if (user) + { + std::string old_nick = user->nick; + Iid iid(chan_name, this->hostname, Iid::Type::Channel); + const bool self = channel->get_self()->nick == old_nick; + const char user_mode = user->get_most_significant_mode(this->sorted_user_modes); + this->bridge.send_nick_change(std::move(iid), old_nick, new_nick, user_mode, self); + user->nick = new_nick; + if (self) + { + channel->get_self()->nick = new_nick; + this->current_nick = new_nick; + } + } + }; + + if (this->get_dummy_channel().joined) + { + change_nick_func("", &this->get_dummy_channel()); + } for (auto it = this->channels.begin(); it != this->channels.end(); ++it) { - const std::string chan_name = it->first; - IrcChannel* channel = it->second.get(); - IrcUser* user = channel->find_user(message.prefix); - if (user) - { - std::string old_nick = user->nick; - Iid iid; - iid.set_local(chan_name); - iid.set_server(this->hostname); - iid.is_channel = true; - const bool self = channel->get_self()->nick == old_nick; - const char user_mode = user->get_most_significant_mode(this->sorted_user_modes); - this->bridge.send_nick_change(std::move(iid), old_nick, new_nick, user_mode, self); - user->nick = new_nick; - if (self) - { - channel->get_self()->nick = new_nick; - this->current_nick = new_nick; - } - } + change_nick_func(it->first, it->second.get()); } } @@ -950,14 +983,26 @@ void IrcClient::on_kick(const IrcMessage& message) IrcChannel* channel = this->get_channel(chan_name); if (!channel->joined) return ; - if (channel->get_self()->nick == target) + const bool self = channel->get_self()->nick == target; + if (self) channel->joined = false; IrcUser author(message.prefix); Iid iid; iid.set_local(chan_name); iid.set_server(this->hostname); - iid.is_channel = true; - this->bridge.kick_muc_user(std::move(iid), target, reason, author.nick); + iid.type = Iid::Type::Channel; + this->bridge.kick_muc_user(std::move(iid), target, reason, author.nick, self); +} + +void IrcClient::on_invite(const IrcMessage& message) +{ + IrcUser author(message.prefix); + Iid iid; + iid.set_local(message.arguments[1]); + iid.set_server(this->hostname); + iid.type = Iid::Type::Channel; + + this->bridge.send_xmpp_invitation(iid, author.nick); } void IrcClient::on_mode(const IrcMessage& message) @@ -976,7 +1021,7 @@ void IrcClient::on_channel_mode(const IrcMessage& message) Iid iid; iid.set_local(message.arguments[0]); iid.set_server(this->hostname); - iid.is_channel = true; + iid.type = Iid::Type::Channel; IrcUser user(message.prefix); std::string mode_arguments; for (size_t i = 1; i < message.arguments.size(); ++i) @@ -1105,7 +1150,7 @@ void IrcClient::leave_dummy_channel(const std::string& exit_message) this->dummy_channel.joined = false; this->dummy_channel.joining = false; this->dummy_channel.remove_all_users(); - this->bridge.send_muc_leave(Iid("%"s + this->hostname), std::string(this->current_nick), exit_message, true); + this->bridge.send_muc_leave(Iid("%"s + this->hostname, this->chantypes), std::string(this->current_nick), exit_message, true); } #ifdef BOTAN_FOUND diff --git a/src/irc/irc_client.hpp b/src/irc/irc_client.hpp index fc3918e..1b4d892 100644 --- a/src/irc/irc_client.hpp +++ b/src/irc/irc_client.hpp @@ -129,6 +129,7 @@ public: * Send the LIST irc command */ void send_list_command(); + void send_invitation(const std::string& chan_name, const std::string& nick); void send_topic_command(const std::string& chan_name, const std::string& topic); /** * Send the QUIT irc command @@ -213,6 +214,10 @@ public: */ void on_empty_topic(const IrcMessage& message); /** + * The IRC server is confirming that the invitation has been forwarded + */ + void on_invited(const IrcMessage& message); + /** * The channel has been completely joined (self presence, topic, all names * received etc), send the self presence and topic to the XMPP user. */ @@ -235,6 +240,10 @@ public: */ void on_nickname_change_too_fast(const IrcMessage& message); /** + * An error when we try to invite a user already in the channel + */ + void on_useronchannel(const IrcMessage& message); + /** * Handles most errors from the server by just forwarding the message to the user. */ void on_generic_error(const IrcMessage& message); @@ -244,6 +253,7 @@ public: void on_welcome_message(const IrcMessage& message); void on_part(const IrcMessage& message); void on_error(const IrcMessage& message); + void on_invite(const IrcMessage& message); void on_nick(const IrcMessage& message); void on_kick(const IrcMessage& message); void on_mode(const IrcMessage& message); @@ -280,8 +290,9 @@ public: const Resolver& get_resolver() const { return this->dns_resolver; } - const std::vector<char>& get_sorted_user_modes() const { return sorted_user_modes; } + const std::vector<char>& get_sorted_user_modes() const { return this->sorted_user_modes; } + std::set<char> get_chantypes() const { return this->chantypes; } private: /** * The hostname of the server we are connected to. diff --git a/src/main.cpp b/src/main.cpp index 53f3193..019dff0 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -12,11 +12,12 @@ #include <atomic> #include <signal.h> +#include <litesql.hpp> // A flag set by the SIGINT signal handler. -static volatile std::atomic<bool> stop(false); +static std::atomic<bool> stop(false); // Flag set by the SIGUSR1/2 signal handler. -static volatile std::atomic<bool> reload(false); +static std::atomic<bool> reload(false); // A flag indicating that we are wanting to exit the process. i.e: if this // flag is set and all connections are closed, we can exit properly. static bool exiting = false; @@ -84,7 +85,7 @@ int main(int ac, char** av) try { open_database(); - } catch (...) { + } catch (const litesql::DatabaseError&) { return 1; } diff --git a/src/xmpp/biboumi_adhoc_commands.cpp b/src/xmpp/biboumi_adhoc_commands.cpp index eec930d..003b901 100644 --- a/src/xmpp/biboumi_adhoc_commands.cpp +++ b/src/xmpp/biboumi_adhoc_commands.cpp @@ -1,9 +1,13 @@ #include <xmpp/biboumi_adhoc_commands.hpp> #include <xmpp/biboumi_component.hpp> +#include <utils/scopeguard.hpp> +#include <bridge/bridge.hpp> #include <config/config.hpp> #include <utils/string.hpp> #include <utils/split.hpp> #include <xmpp/jid.hpp> +#include <algorithm> +#include <iomanip> #include <biboumi.h> @@ -11,9 +15,9 @@ #include <database/database.hpp> #endif -#include <louloulibs.h> - -#include <algorithm> +#ifndef HAS_PUT_TIME +#include <ctime> +#endif using namespace std::string_literals; @@ -114,6 +118,96 @@ void DisconnectUserStep2(XmppComponent& xmpp_component, AdhocSession& session, X } #ifdef USE_DATABASE + +void ConfigureGlobalStep1(XmppComponent&, AdhocSession& session, XmlNode& command_node) +{ + const Jid owner(session.get_owner_jid()); + const Jid target(session.get_target_jid()); + + auto options = Database::get_global_options(owner.bare()); + + XmlNode x("jabber:x:data:x"); + x["type"] = "form"; + XmlNode title("title"); + title.set_inner("Configure some global default settings."); + x.add_child(std::move(title)); + XmlNode instructions("instructions"); + instructions.set_inner("Edit the form, to configure your global settings for the component."); + x.add_child(std::move(instructions)); + + XmlNode required("required"); + + XmlNode max_histo_length("field"); + 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"; + + XmlNode value("value"); + value.set_inner(std::to_string(options.maxHistoryLength.value())); + max_histo_length.add_child(std::move(value)); + x.add_child(std::move(max_histo_length)); + + XmlNode record_history("field"); + 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"; + + value.set_name("value"); + if (options.recordHistory.value()) + value.set_inner("true"); + else + value.set_inner("false"); + record_history.add_child(std::move(value)); + x.add_child(std::move(record_history)); + + command_node.add_child(std::move(x)); +} + +void ConfigureGlobalStep2(XmppComponent& xmpp_component, AdhocSession& session, XmlNode& command_node) +{ + BiboumiComponent& biboumi_component = static_cast<BiboumiComponent&>(xmpp_component); + + const XmlNode* x = command_node.get_child("x", "jabber:x:data"); + if (x) + { + const Jid owner(session.get_owner_jid()); + auto options = Database::get_global_options(owner.bare()); + for (const XmlNode* field: x->get_children("field", "jabber:x:data")) + { + const XmlNode* value = field->get_child("value", "jabber:x:data"); + + if (field->get_tag("var") == "max_history_length" && + value && !value->get_inner().empty()) + options.maxHistoryLength = value->get_inner(); + else if (field->get_tag("var") == "record_history" && + value && !value->get_inner().empty()) + { + options.recordHistory = to_bool(value->get_inner()); + Bridge* bridge = biboumi_component.find_user_bridge(owner.bare()); + if (bridge) + bridge->set_record_history(options.recordHistory.value()); + } + } + + options.update(); + + command_node.delete_all_children(); + XmlNode note("note"); + note["type"] = "info"; + note.set_inner("Configuration successfully applied."); + command_node.add_child(std::move(note)); + return; + } + XmlNode error(ADHOC_NS":error"); + error["type"] = "modify"; + XmlNode condition(STANZA_NS":bad-request"); + error.add_child(std::move(condition)); + command_node.add_child(std::move(error)); + session.terminate(); +} + void ConfigureIrcServerStep1(XmppComponent&, AdhocSession& session, XmlNode& command_node) { const Jid owner(session.get_owner_jid()); @@ -315,7 +409,7 @@ void ConfigureIrcServerStep2(XmppComponent&, AdhocSession& session, XmlNode& com } else if (field->get_tag("var") == "verify_cert" && value - && !value->get_inner().empty()) + && !value->get_inner().empty()) { auto val = to_bool(value->get_inner()); options.verifyCert = val; @@ -381,7 +475,7 @@ void ConfigureIrcChannelStep1(XmppComponent&, AdhocSession& session, XmlNode& co { const Jid owner(session.get_owner_jid()); const Jid target(session.get_target_jid()); - const Iid iid(target.local); + const Iid iid(target.local, {}); auto options = Database::get_irc_channel_options_with_server_default(owner.local + "@" + owner.domain, iid.get_server(), iid.get_local()); @@ -434,7 +528,7 @@ void ConfigureIrcChannelStep2(XmppComponent&, AdhocSession& session, XmlNode& co { const Jid owner(session.get_owner_jid()); const Jid target(session.get_target_jid()); - const Iid iid(target.local); + const Iid iid(target.local, {}); auto options = Database::get_irc_channel_options(owner.local + "@" + owner.domain, iid.get_server(), iid.get_local()); for (const XmlNode* field: x->get_children("field", "jabber:x:data")) @@ -442,7 +536,7 @@ void ConfigureIrcChannelStep2(XmppComponent&, AdhocSession& session, XmlNode& co const XmlNode* value = field->get_child("value", "jabber:x:data"); if (field->get_tag("var") == "encoding_out" && - value && !value->get_inner().empty()) + value && !value->get_inner().empty()) options.encodingOut = value->get_inner(); else if (field->get_tag("var") == "encoding_in" && @@ -633,3 +727,74 @@ void DisconnectUserFromServerStep3(XmppComponent& xmpp_component, AdhocSession& note.set_inner(msg); command_node.add_child(std::move(note)); } + +void GetIrcConnectionInfoStep1(XmppComponent& component, AdhocSession& session, XmlNode& command_node) +{ + BiboumiComponent& biboumi_component = static_cast<BiboumiComponent&>(component); + + const Jid owner(session.get_owner_jid()); + const Jid target(session.get_target_jid()); + + std::string message{}; + + // As the function is exited, set the message in the response. + utils::ScopeGuard sg([&message, &command_node]() + { + command_node.delete_all_children(); + XmlNode note("note"); + note["type"] = "info"; + note.set_inner(message); + command_node.add_child(std::move(note)); + }); + + Bridge* bridge = biboumi_component.get_user_bridge(owner.bare()); + if (!bridge) + { + message = "You are not connected to anything."; + return; + } + + std::string hostname; + if ((hostname = Config::get("fixed_irc_server", "")).empty()) + hostname = target.local; + + IrcClient* irc = bridge->find_irc_client(hostname); + if (!irc || !irc->is_connected()) + { + message = "You are not connected to the IRC server "s + hostname; + return; + } + + std::ostringstream ss; + ss << "Connected to IRC server " << irc->get_hostname() << " on port " << irc->get_port(); + if (irc->is_using_tls()) + ss << " (using TLS)"; + const std::time_t now_c = std::chrono::system_clock::to_time_t(irc->connection_date); +#ifdef HAS_PUT_TIME + ss << " since " << std::put_time(std::localtime(&now_c), "%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)); + if (res > 0) + ss << " since " << buf; +#endif + ss << " (" << std::chrono::duration_cast<std::chrono::seconds>(std::chrono::system_clock::now() - irc->connection_date).count() << " seconds ago)."; + + for (const auto& it: bridge->resources_in_chan) + { + const auto& channel_key = it.first; + const auto& irc_hostname = std::get<1>(channel_key); + const auto& resources = it.second; + + if (irc_hostname == irc->get_hostname() && !resources.empty()) + { + const auto& channel_name = std::get<0>(channel_key); + ss << "\n" << channel_name << " from " << resources.size() << " resource" << (resources.size() > 1 ? "s": "") << ": "; + for (const auto& resource: resources) + ss << resource << " "; + } + } + + message = ss.str(); +} diff --git a/src/xmpp/biboumi_adhoc_commands.hpp b/src/xmpp/biboumi_adhoc_commands.hpp index 2763a9f..b5fce61 100644 --- a/src/xmpp/biboumi_adhoc_commands.hpp +++ b/src/xmpp/biboumi_adhoc_commands.hpp @@ -10,6 +10,9 @@ class XmppComponent; void DisconnectUserStep1(XmppComponent&, AdhocSession& session, XmlNode& command_node); void DisconnectUserStep2(XmppComponent&, AdhocSession& session, XmlNode& command_node); +void ConfigureGlobalStep1(XmppComponent&, AdhocSession& session, XmlNode& command_node); +void ConfigureGlobalStep2(XmppComponent&, AdhocSession& session, XmlNode& command_node); + void ConfigureIrcServerStep1(XmppComponent&, AdhocSession& session, XmlNode& command_node); void ConfigureIrcServerStep2(XmppComponent&, AdhocSession& session, XmlNode& command_node); @@ -20,4 +23,4 @@ void DisconnectUserFromServerStep1(XmppComponent&, AdhocSession& session, XmlNod void DisconnectUserFromServerStep2(XmppComponent&, AdhocSession& session, XmlNode& command_node); void DisconnectUserFromServerStep3(XmppComponent&, AdhocSession& session, XmlNode& command_node); - +void GetIrcConnectionInfoStep1(XmppComponent&, AdhocSession& session, XmlNode& command_node); diff --git a/src/xmpp/biboumi_component.cpp b/src/xmpp/biboumi_component.cpp index a6aac21..d6782e2 100644 --- a/src/xmpp/biboumi_component.cpp +++ b/src/xmpp/biboumi_component.cpp @@ -8,23 +8,27 @@ #include <xmpp/biboumi_adhoc_commands.hpp> #include <bridge/list_element.hpp> #include <config/config.hpp> -#include <xmpp/jid.hpp> #include <utils/sha1.hpp> +#include <utils/time.hpp> +#include <xmpp/jid.hpp> #include <stdexcept> #include <iostream> -#include <stdio.h> +#include <cstdlib> #include <louloulibs.h> #include <biboumi.h> -#include <uuid.h> +#include <uuid/uuid.h> #ifdef SYSTEMD_FOUND # include <systemd/sd-daemon.h> #endif +#include <database/database.hpp> +#include <bridge/result_set_management.hpp> + using namespace std::string_literals; static std::set<std::string> kickable_errors{ @@ -53,33 +57,30 @@ BiboumiComponent::BiboumiComponent(std::shared_ptr<Poller> poller, const std::st this->stanza_handlers.emplace("iq", std::bind(&BiboumiComponent::handle_iq, this,std::placeholders::_1)); - this->adhoc_commands_handler.get_commands() = { - {"ping", AdhocCommand({&PingStep1}, "Do a ping", false)}, - {"hello", AdhocCommand({&HelloStep1, &HelloStep2}, "Receive a custom greeting", false)}, - {"disconnect-user", AdhocCommand({&DisconnectUserStep1, &DisconnectUserStep2}, "Disconnect selected users from the gateway", true)}, - {"disconnect-from-irc-servers", AdhocCommand({&DisconnectUserFromServerStep1, &DisconnectUserFromServerStep2, &DisconnectUserFromServerStep3}, "Disconnect from the selected IRC servers", false)}, - {"reload", AdhocCommand({&Reload}, "Reload biboumi’s configuration", true)} - }; + this->adhoc_commands_handler.add_command("ping", {{&PingStep1}, "Do a ping", false}); + this->adhoc_commands_handler.add_command("hello", {{&HelloStep1, &HelloStep2}, "Receive a custom greeting", false}); + this->adhoc_commands_handler.add_command("disconnect-user", {{&DisconnectUserStep1, &DisconnectUserStep2}, "Disconnect selected users from the gateway", true}); + this->adhoc_commands_handler.add_command("disconnect-from-irc-server", {{&DisconnectUserFromServerStep1, &DisconnectUserFromServerStep2, &DisconnectUserFromServerStep3}, "Disconnect from the selected IRC servers", false}); + this->adhoc_commands_handler.add_command("reload", {{&Reload}, "Reload biboumi’s configuration", true}); + + AdhocCommand get_irc_connection_info{{&GetIrcConnectionInfoStep1}, "Returns various information about your connection to this IRC server.", false}; + if (!Config::get("fixed_irc_server", "").empty()) + this->adhoc_commands_handler.add_command("get-irc-connection-info", get_irc_connection_info); + else + this->irc_server_adhoc_commands_handler.add_command("get-irc-connection-info", get_irc_connection_info); #ifdef USE_DATABASE AdhocCommand configure_server_command({&ConfigureIrcServerStep1, &ConfigureIrcServerStep2}, "Configure a few settings for that IRC server", false); + AdhocCommand configure_global_command({&ConfigureGlobalStep1, &ConfigureGlobalStep2}, "Configure a few settings", false); + if (!Config::get("fixed_irc_server", "").empty()) - { - this->adhoc_commands_handler.get_commands().emplace(std::make_pair("configure", - configure_server_command)); - } -#endif + this->adhoc_commands_handler.add_command("configure", configure_server_command); + else + this->adhoc_commands_handler.add_command("configure", configure_global_command); - this->irc_server_adhoc_commands_handler.get_commands() = { -#ifdef USE_DATABASE - {"configure", configure_server_command}, + this->irc_server_adhoc_commands_handler.add_command("configure", configure_server_command); + this->irc_channel_adhoc_commands_handler.add_command("configure", {{&ConfigureIrcChannelStep1, &ConfigureIrcChannelStep2}, "Configure a few settings for that IRC channel", false}); #endif - }; - this->irc_channel_adhoc_commands_handler.get_commands() = { -#ifdef USE_DATABASE - {"configure", AdhocCommand({&ConfigureIrcChannelStep1, &ConfigureIrcChannelStep2}, "Configure a few settings for that IRC channel", false)}, -#endif - }; } void BiboumiComponent::shutdown() @@ -126,7 +127,7 @@ void BiboumiComponent::handle_presence(const Stanza& stanza) Bridge* bridge = this->get_user_bridge(from_str); Jid to(to_str); Jid from(from_str); - Iid iid(to.local); + Iid iid(to.local, bridge); // An error stanza is sent whenever we exit this function without // disabling this scopeguard. If error_type and error_name are not @@ -142,7 +143,7 @@ void BiboumiComponent::handle_presence(const Stanza& stanza) }); try { - if (iid.is_channel && !iid.get_server().empty()) + if (iid.type == Iid::Type::Channel && !iid.get_server().empty()) { // presence toward a MUC that corresponds to an irc channel, or a // dummy channel if iid.chan is empty if (type.empty()) @@ -163,7 +164,7 @@ void BiboumiComponent::handle_presence(const Stanza& stanza) } else { - // An user wants to join an invalid IRC channel, return a presence error to him + // A user wants to join an invalid IRC channel, return a presence error to him/her if (type.empty()) this->send_invalid_room_error(to.local, to.resource, from_str); } @@ -180,29 +181,30 @@ void BiboumiComponent::handle_presence(const Stanza& stanza) void BiboumiComponent::handle_message(const Stanza& stanza) { - std::string from = stanza.get_tag("from"); + std::string from_str = stanza.get_tag("from"); std::string id = stanza.get_tag("id"); std::string to_str = stanza.get_tag("to"); std::string type = stanza.get_tag("type"); - if (from.empty()) + if (from_str.empty()) return; if (type.empty()) type = "normal"; - Bridge* bridge = this->get_user_bridge(from); + Bridge* bridge = this->get_user_bridge(from_str); + Jid from(from_str); Jid to(to_str); - Iid iid(to.local); + Iid iid(to.local, bridge); std::string error_type("cancel"); std::string error_name("internal-server-error"); utils::ScopeGuard stanza_error([&](){ - this->send_stanza_error("message", from, to_str, id, + this->send_stanza_error("message", from_str, to_str, id, error_type, error_name, ""); }); const XmlNode* body = stanza.get_child("body", COMPONENT_NS); try { // catch IRCNotConnected exceptions - if (type == "groupchat" && iid.is_channel) + if (type == "groupchat" && iid.type == Iid::Type::Channel) { if (body && !body->get_inner().empty()) { @@ -216,7 +218,7 @@ void BiboumiComponent::handle_message(const Stanza& stanza) { const XmlNode* error = stanza.get_child("error", COMPONENT_NS); // Only a set of errors are considered “fatal”. If we encounter one of - // them, we purge (we disconnect the user from all the IRC servers). + // them, we purge (we disconnect that resource from all the IRC servers) // We consider this to be true, unless the error condition is // specified and is not in the kickable_errors set bool kickable_error = true; @@ -227,38 +229,49 @@ void BiboumiComponent::handle_message(const Stanza& stanza) kickable_error = false; } if (kickable_error) - bridge->shutdown("Error from remote client"); + bridge->remove_resource(from.resource, "Error from remote client"); } else if (type == "chat") { if (body && !body->get_inner().empty()) { // a message for nick!server - if (iid.is_user && !iid.get_local().empty()) + if (iid.type == Iid::Type::User && !iid.get_local().empty()) { bridge->send_private_message(iid, body->get_inner()); bridge->remove_preferred_from_jid(iid.get_local()); } - else if (!iid.is_user && !to.resource.empty()) + else if (iid.type != Iid::Type::User && !to.resource.empty()) { // a message for chan%server@biboumi/Nick or // server@biboumi/Nick // Convert that into a message to nick!server - Iid user_iid(utils::tolower(to.resource) + "!" + iid.get_server()); + Iid user_iid(utils::tolower(to.resource), iid.get_server(), Iid::Type::User); bridge->send_private_message(user_iid, body->get_inner()); bridge->set_preferred_from_jid(user_iid.get_local(), to_str); } - else if (!iid.is_user && !iid.is_channel) + else if (iid.type == Iid::Type::Server) { // Message sent to the server JID // Convert the message body into a raw IRC message bridge->send_raw_message(iid.get_server(), body->get_inner()); } } } - else if (iid.is_user) - this->send_invalid_user_error(to.local, from); + else if (type == "normal" && iid.type == Iid::Type::Channel) + { + if (const XmlNode* x = stanza.get_child("x", MUC_USER_NS)) + if (const XmlNode* invite = x->get_child("invite", MUC_USER_NS)) + { + const auto invite_to = invite->get_tag("to"); + if (!invite_to.empty()) + { + bridge->send_irc_invitation(iid, invite_to); + } + } + + } } catch (const IRCNotConnected& ex) { - this->send_stanza_error("message", from, to_str, id, + this->send_stanza_error("message", from_str, to_str, id, "cancel", "remote-server-not-found", "Not connected to IRC server "s + ex.hostname, true); @@ -321,7 +334,7 @@ void BiboumiComponent::handle_iq(const Stanza& stanza) std::string affiliation = child->get_tag("affiliation"); if (!nick.empty()) { - Iid iid(to.local); + Iid iid(to.local, {}); if (role == "none") { // This is a kick std::string reason; @@ -345,15 +358,17 @@ void BiboumiComponent::handle_iq(const Stanza& stanza) // Depending on the 'to' jid in the request, we use one adhoc // command handler or an other - Iid iid(to.local); + Iid iid(to.local, {}); AdhocCommandsHandler* adhoc_handler; - if (!to.local.empty() && !iid.is_user && !iid.is_channel) - adhoc_handler = &this->irc_server_adhoc_commands_handler; - else if (!to.local.empty() && iid.is_channel) - adhoc_handler = &this->irc_channel_adhoc_commands_handler; - else + if (to.local.empty()) adhoc_handler = &this->adhoc_commands_handler; - + else + { + if (iid.type == Iid::Type::Server) + adhoc_handler = &this->irc_server_adhoc_commands_handler; + else + adhoc_handler = &this->irc_channel_adhoc_commands_handler; + } // Execute the command, if any, and get a result XmlNode that we // insert in our response XmlNode inner_node = adhoc_handler->handle_request(from, to_str, *query); @@ -365,15 +380,23 @@ void BiboumiComponent::handle_iq(const Stanza& stanza) this->send_stanza(response); stanza_error.disable(); } +#ifdef USE_DATABASE + else if ((query = stanza.get_child("query", MAM_NS))) + { + if (this->handle_mam_request(stanza)) + stanza_error.disable(); + } +#endif } else if (type == "get") { const XmlNode* query; if ((query = stanza.get_child("query", DISCO_INFO_NS))) { // Disco info + Iid iid(to.local, {'#', '&'}); + const std::string node = query->get_tag("node"); if (to_str == this->served_hostname) { - const std::string node = query->get_tag("node"); if (node.empty()) { // On the gateway itself @@ -381,16 +404,32 @@ void BiboumiComponent::handle_iq(const Stanza& stanza) stanza_error.disable(); } } + else if (iid.type == Iid::Type::Server) + { + if (node.empty()) + { + this->send_irc_server_disco_info(id, from, to_str); + stanza_error.disable(); + } + } + else if (iid.type == Iid::Type::Channel) + { + if (node == MUC_TRAFFIC_NS) + { + this->send_irc_channel_muc_traffic_info(id, from, to_str); + stanza_error.disable(); + } + } } else if ((query = stanza.get_child("query", VERSION_NS))) { - Iid iid(to.local); - if (iid.is_user || - (iid.is_channel && !to.resource.empty())) + Iid iid(to.local, bridge); + if ((iid.type == Iid::Type::Channel && !to.resource.empty()) || + (iid.type == Iid::Type::User)) { // Get the IRC user version std::string target; - if (iid.is_user) + if (iid.type == Iid::Type::User) target = iid.get_local(); else target = to.resource; @@ -406,7 +445,7 @@ void BiboumiComponent::handle_iq(const Stanza& stanza) } else if ((query = stanza.get_child("query", DISCO_ITEMS_NS))) { - Iid iid(to.local); + Iid iid(to.local, bridge); const std::string node = query->get_tag("node"); if (node == ADHOC_NS) { @@ -419,7 +458,7 @@ void BiboumiComponent::handle_iq(const Stanza& stanza) this->adhoc_commands_handler); stanza_error.disable(); } - else if (!iid.is_user && !iid.is_channel) + else if (iid.type == Iid::Type::Server) { // Get the server's adhoc commands this->send_adhoc_commands_list(id, from, to_str, (Config::get("admin", "") == @@ -427,7 +466,7 @@ void BiboumiComponent::handle_iq(const Stanza& stanza) this->irc_server_adhoc_commands_handler); stanza_error.disable(); } - else if (!iid.is_user && iid.is_channel) + else if (iid.type == Iid::Type::Channel) { // Get the channel's adhoc commands this->send_adhoc_commands_list(id, from, to_str, (Config::get("admin", "") == @@ -436,21 +475,36 @@ void BiboumiComponent::handle_iq(const Stanza& stanza) stanza_error.disable(); } } - else if (node.empty() && !iid.is_user && !iid.is_channel) + else if (node.empty() && iid.type == Iid::Type::Server) { // Disco on an IRC server: get the list of channels - bridge->send_irc_channel_list_request(iid, id, from); + ResultSetInfo rs_info; + const XmlNode* set_node = query->get_child("set", RSM_NS); + if (set_node) + { + const XmlNode* after = set_node->get_child("after", RSM_NS); + if (after) + rs_info.after = after->get_inner(); + const XmlNode* before = set_node->get_child("before", RSM_NS); + if (before) + rs_info.before = before->get_inner(); + const XmlNode* max = set_node->get_child("max", RSM_NS); + if (max) + rs_info.max = std::atoi(max->get_inner().data()); + + } + bridge->send_irc_channel_list_request(iid, id, from, std::move(rs_info)); stanza_error.disable(); } } else if ((query = stanza.get_child("ping", PING_NS))) { - Iid iid(to.local); - if (iid.is_user) + Iid iid(to.local, bridge); + if (iid.type == Iid::Type::User) { // Ping any user (no check on the nick done ourself) bridge->send_irc_user_ping_request(iid.get_server(), iid.get_local(), id, from, to_str); } - else if (iid.is_channel && !to.resource.empty()) + else if (iid.type == Iid::Type::Channel && !to.resource.empty()) { // Ping a room participant (we check if the nick is in the room) bridge->send_irc_participant_ping_request(iid, to.resource, id, from, to_str); @@ -481,7 +535,7 @@ void BiboumiComponent::handle_iq(const Stanza& stanza) version = version_node->get_inner(); if (os_node) os = os_node->get_inner(); - const Iid iid(to.local); + const Iid iid(to.local, bridge); bridge->send_xmpp_version_to_irc(iid, name, version, os); } else @@ -508,6 +562,96 @@ void BiboumiComponent::handle_iq(const Stanza& stanza) error_name = "feature-not-implemented"; } +#ifdef USE_DATABASE +bool BiboumiComponent::handle_mam_request(const Stanza& stanza) +{ + std::string id = stanza.get_tag("id"); + Jid from(stanza.get_tag("from")); + Jid to(stanza.get_tag("to")); + + const XmlNode* query = stanza.get_child("query", MAM_NS); + std::string query_id; + if (query) + query_id = query->get_tag("queryid"); + + Iid iid(to.local, {'#', '&'}); + if (iid.type == Iid::Type::Channel && to.resource.empty()) + { + std::string start; + std::string end; + const XmlNode* x = query->get_child("x", DATAFORM_NS); + if (x) + { + const XmlNode* value; + const auto fields = x->get_children("field", DATAFORM_NS); + for (const auto& field: fields) + { + if (field->get_tag("var") == "start") + { + value = field->get_child("value", DATAFORM_NS); + if (value) + start = value->get_inner(); + } + else if (field->get_tag("var") == "end") + { + value = field->get_child("value", DATAFORM_NS); + if (value) + end = value->get_inner(); + } + } + } + const auto lines = Database::get_muc_logs(from.bare(), iid.get_local(), iid.get_server(), -1, start, end); + for (const db::MucLogLine& line: lines) + { + if (!line.nick.value().empty()) + this->send_archived_message(line, to.full(), from.full(), query_id); + } + this->send_iq_result_full_jid(id, from.full(), to.full()); + return true; + } + return false; +} + +void BiboumiComponent::send_archived_message(const db::MucLogLine& log_line, const std::string& from, const std::string& to, + const std::string& queryid) +{ + Stanza message("message"); + message["from"] = from; + message["to"] = to; + + XmlNode result("result"); + result["xmlns"] = MAM_NS; + if (!queryid.empty()) + result["queryid"] = queryid; + result["id"] = log_line.uuid.value(); + + XmlNode forwarded("forwarded"); + forwarded["xmlns"] = FORWARD_NS; + + XmlNode delay("delay"); + delay["xmlns"] = DELAY_NS; + delay["stamp"] = utils::to_string(log_line.date.value().timeStamp()); + + forwarded.add_child(std::move(delay)); + + XmlNode submessage("message"); + submessage["xmlns"] = CLIENT_NS; + submessage["from"] = from + "/" + log_line.nick.value(); + submessage["type"] = "groupchat"; + + XmlNode body("body"); + body.set_inner(log_line.body.value()); + submessage.add_child(std::move(body)); + + forwarded.add_child(std::move(submessage)); + result.add_child(std::move(forwarded)); + message.add_child(std::move(result)); + + this->send_stanza(message); +} + +#endif + Bridge* BiboumiComponent::get_user_bridge(const std::string& user_jid) { auto bare_jid = Jid{user_jid}.bare(); @@ -517,8 +661,7 @@ Bridge* BiboumiComponent::get_user_bridge(const std::string& user_jid) } catch (const std::out_of_range& exception) { - this->bridges.emplace(bare_jid, std::make_unique<Bridge>(bare_jid, *this, this->poller)); - return this->bridges.at(bare_jid).get(); + return this->bridges.emplace(bare_jid, std::make_unique<Bridge>(bare_jid, *this, this->poller)).first->second.get(); } } @@ -557,7 +700,32 @@ void BiboumiComponent::send_self_disco_info(const std::string& id, const std::st identity["type"] = "irc"; identity["name"] = "Biboumi XMPP-IRC gateway"; query.add_child(std::move(identity)); - for (const char* ns: {DISCO_INFO_NS, MUC_NS, ADHOC_NS}) + for (const char* ns: {DISCO_INFO_NS, MUC_NS, ADHOC_NS, PING_NS, MAM_NS, VERSION_NS}) + { + XmlNode feature("feature"); + feature["var"] = ns; + query.add_child(std::move(feature)); + } + iq.add_child(std::move(query)); + this->send_stanza(iq); +} + +void BiboumiComponent::send_irc_server_disco_info(const std::string& id, const std::string& jid_to, const std::string& jid_from) +{ + Jid from(jid_from); + Stanza iq("iq"); + iq["type"] = "result"; + iq["id"] = id; + iq["to"] = jid_to; + iq["from"] = jid_from; + XmlNode query("query"); + query["xmlns"] = DISCO_INFO_NS; + XmlNode identity("identity"); + identity["category"] = "conference"; + identity["type"] = "irc"; + identity["name"] = "IRC server "s + from.local + " over Biboumi"; + query.add_child(std::move(identity)); + for (const char* ns: {DISCO_INFO_NS, ADHOC_NS, PING_NS, VERSION_NS}) { XmlNode feature("feature"); feature["var"] = ns; @@ -567,6 +735,25 @@ void BiboumiComponent::send_self_disco_info(const std::string& id, const std::st this->send_stanza(iq); } +void BiboumiComponent::send_irc_channel_muc_traffic_info(const std::string id, const std::string& jid_from, const std::string& jid_to) +{ + Stanza iq("iq"); + iq["type"] = "result"; + iq["id"] = id; + iq["from"] = jid_from; + iq["to"] = jid_to; + + XmlNode query("query"); + query["xmlns"] = DISCO_INFO_NS; + query["node"] = MUC_TRAFFIC_NS; + // We drop all “special” traffic (like xhtml-im, chatstates, etc), so + // don’t include any <feature/> + iq.add_child(std::move(query)); + + this->send_stanza(iq); + +} + void BiboumiComponent::send_iq_version_request(const std::string& from, const std::string& jid_to) { @@ -604,15 +791,16 @@ void BiboumiComponent::send_ping_request(const std::string& from, "the response mismatches the 'from' of the request"); } else - bridge->send_irc_ping_result(from, id); + bridge->send_irc_ping_result({from, bridge}, id); }; this->waiting_iq[id] = result_cb; } -void BiboumiComponent::send_iq_room_list_result(const std::string& id, - const std::string& to_jid, - const std::string& from, - const std::vector<ListElement>& rooms_list) +void BiboumiComponent::send_iq_room_list_result(const std::string& id, const std::string& to_jid, + const std::string& from, const ChannelList& channel_list, + std::vector<ListElement>::const_iterator begin, + std::vector<ListElement>::const_iterator end, + const ResultSetInfo& rs_info) { Stanza iq("iq"); iq["from"] = from + "@" + this->served_hostname; @@ -621,12 +809,60 @@ void BiboumiComponent::send_iq_room_list_result(const std::string& id, iq["type"] = "result"; XmlNode query("query"); query["xmlns"] = DISCO_ITEMS_NS; - for (const auto& room: rooms_list) + + for (auto it = begin; it != end; ++it) { XmlNode item("item"); - item["jid"] = room.channel + "%" + from + "@" + this->served_hostname; + item["jid"] = it->channel + "@" + this->served_hostname; query.add_child(std::move(item)); } + + if ((rs_info.max >= 0 || !rs_info.after.empty() || !rs_info.before.empty())) + { + XmlNode set_node("set"); + set_node["xmlns"] = RSM_NS; + + if (begin != channel_list.channels.cend()) + { + XmlNode first_node("first"); + first_node["index"] = std::to_string(std::distance(channel_list.channels.cbegin(), begin)); + first_node.set_inner(begin->channel + "@" + this->served_hostname); + set_node.add_child(std::move(first_node)); + } + if (end != channel_list.channels.cbegin()) + { + XmlNode last_node("last"); + last_node.set_inner(std::prev(end)->channel + "@" + this->served_hostname); + set_node.add_child(std::move(last_node)); + } + if (channel_list.complete) + { + XmlNode count_node("count"); + count_node.set_inner(std::to_string(channel_list.channels.size())); + set_node.add_child(std::move(count_node)); + } + query.add_child(std::move(set_node)); + } + iq.add_child(std::move(query)); this->send_stanza(iq); } + +void BiboumiComponent::send_invitation(const std::string& room_target, + const std::string& jid_to, + const std::string& author_nick) +{ + Stanza message("message"); + message["from"] = room_target + "@" + this->served_hostname; + message["to"] = jid_to; + XmlNode x("x"); + x["xmlns"] = MUC_USER_NS; + XmlNode invite("invite"); + if (author_nick.empty()) + invite["from"] = room_target + "@" + this->served_hostname; + else + invite["from"] = room_target + "@" + this->served_hostname + "/" + author_nick; + x.add_child(std::move(invite)); + message.add_child(std::move(x)); + this->send_stanza(message); +} diff --git a/src/xmpp/biboumi_component.hpp b/src/xmpp/biboumi_component.hpp index 24d768a..999001f 100644 --- a/src/xmpp/biboumi_component.hpp +++ b/src/xmpp/biboumi_component.hpp @@ -9,6 +9,10 @@ #include <string> #include <map> +namespace db +{ +class MucLogLine; +} struct ListElement; /** @@ -58,6 +62,15 @@ public: */ void send_self_disco_info(const std::string& id, const std::string& jid_to); /** + * Send a result IQ with the disco informations regarding IRC server JIDs. + */ + void send_irc_server_disco_info(const std::string& id, const std::string& jid_to, const std::string& jid_from); + /** + * Sends the allowed namespaces in MUC message, according to + * 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_from, const std::string& jid_to); + /** * Send an iq version request */ void send_iq_version_request(const std::string& from, @@ -71,9 +84,10 @@ public: /** * Send the channels list in one big stanza */ - void send_iq_room_list_result(const std::string& id, const std::string& to_jid, - const std::string& from, - const std::vector<ListElement>& rooms_list); + void send_iq_room_list_result(const std::string& id, const std::string& to_jid, const std::string& from, + const ChannelList& channel_list, std::vector<ListElement>::const_iterator begin, + std::vector<ListElement>::const_iterator end, const ResultSetInfo& rs_info); + void send_invitation(const std::string& room_target, const std::string& jid_to, const std::string& author_nick); /** * Handle the various stanza types */ @@ -81,13 +95,19 @@ public: void handle_message(const Stanza& stanza); void handle_iq(const Stanza& stanza); -private: +#ifdef USE_DATABASE + bool handle_mam_request(const Stanza& stanza); + void send_archived_message(const db::MucLogLine& log_line, const std::string& from, const std::string& to, + const std::string& queryid); +#endif + /** * Return the bridge associated with the bare JID. Create a new one * if none already exist. */ Bridge* get_user_bridge(const std::string& user_jid); +private: /** * A map of id -> callback. When we want to wait for an iq result, we add * the callback to this map, with the iq id as the key. When an iq result diff --git a/tests/config.cpp b/tests/config.cpp index ddea151..a6fa92a 100644 --- a/tests/config.cpp +++ b/tests/config.cpp @@ -1,9 +1,14 @@ #include "catch.hpp" +#include "io_tester.hpp" + +#include <iostream> #include <config/config.hpp> TEST_CASE("Config basic") { + // Disable all output for this test + IoTester<std::ostream> out(std::cout); // Write a value in the config file Config::read_conf("test.cfg"); Config::set("coucou", "bonjour", true); diff --git a/tests/dns.cpp b/tests/dns.cpp deleted file mode 100644 index c3eda7b..0000000 --- a/tests/dns.cpp +++ /dev/null @@ -1,91 +0,0 @@ -#include "catch.hpp" - -#include <network/dns_handler.hpp> -#include <network/resolver.hpp> -#include <network/poller.hpp> - -#include <utils/timed_events.hpp> - -TEST_CASE("DNS resolver") -{ - Resolver resolver; - Resolver resolver2; - Resolver resolver3; - - /** - * If we are using cares, we need to run a poller loop until each - * resolution is finished. Without cares we get the result before - * resolve() returns because it’s blocking. - */ -#ifdef CARES_FOUND - auto p = std::make_shared<Poller>(); - - const auto loop = [&p]() - { - do - { - DNSHandler::instance.watch_dns_sockets(p); - } - while (p->poll(utils::no_timeout) != -1); - }; -#else - // We don’t need to do anything if we are not using cares. - const auto loop = [](){}; -#endif - - std::string hostname; - std::string port = "6667"; - - bool success = true; - - const auto error_cb = [&success](const std::string& hostname) - { - return [&success, hostname](const char *msg) - { - INFO("Failed to resolve " << hostname << ":" << msg); - success = false; - }; - }; - const auto success_cb = [&success](const std::string& hostname) - { - return [&success, hostname](const struct addrinfo *addr) - { - INFO("Successfully resolved " << hostname << ": " << addr_to_string(addr)); - success = true; - }; - }; - - hostname = "example.com"; - resolver.resolve(hostname, port, - success_cb(hostname), error_cb(hostname)); - hostname = "poez.io"; - resolver2.resolve(hostname, port, - success_cb(hostname), error_cb(hostname)); - hostname = "louiz.org"; - resolver3.resolve(hostname, port, - success_cb(hostname), error_cb(hostname)); - loop(); - CHECK(success); - - hostname = "this.should.fail.because.it.is..misformatted"; - resolver.resolve(hostname, port, - success_cb(hostname), error_cb(hostname)); - loop(); - CHECK(!success); - - hostname = "this.should.fail.because.it.is.does.not.exist.invalid"; - resolver.resolve(hostname, port, - success_cb(hostname), error_cb(hostname)); - loop(); - CHECK(!success); - - hostname = "localhost"; - resolver.resolve(hostname, port, - success_cb(hostname), error_cb(hostname)); - loop(); - CHECK(success); - -#ifdef CARES_FOUND - DNSHandler::instance.destroy(); -#endif -} diff --git a/tests/encoding.cpp b/tests/encoding.cpp index 389cf23..b5192ff 100644 --- a/tests/encoding.cpp +++ b/tests/encoding.cpp @@ -11,7 +11,6 @@ TEST_CASE("UTF-8 validation") CHECK_FALSE(utils::is_valid_utf8("\xFE\xFE\xFF\xFF")); std::string in = "Biboumi ╯°□°)╯︵ ┻━┻"; - INFO(in); CHECK(utils::is_valid_utf8(in.data())); } @@ -49,7 +48,6 @@ TEST_CASE("Remove invalid XML chars") { std::string without_ctrl_char("𤭢€¢$"); std::string in = "Biboumi ╯°□°)╯︵ ┻━┻"; - INFO(in); CHECK(utils::remove_invalid_xml_chars(without_ctrl_char) == without_ctrl_char); CHECK(utils::remove_invalid_xml_chars(in) == in); CHECK(utils::remove_invalid_xml_chars("\acouco\u0008u\uFFFEt\uFFFFe\r\n♥") == "coucoute\r\n♥"); diff --git a/tests/end_to_end/__main__.py b/tests/end_to_end/__main__.py index 4348197..7658d92 100644 --- a/tests/end_to_end/__main__.py +++ b/tests/end_to_end/__main__.py @@ -13,6 +13,8 @@ import os from functools import partial from slixmpp.xmlstream.matcher.base import MatcherBase +if not hasattr(asyncio, "ensure_future"): + asyncio.ensure_future = getattr(asyncio, "async") class MatchAll(MatcherBase): """match everything""" @@ -57,7 +59,7 @@ class XMPPComponent(slixmpp.BaseXMPP): self.add_event_handler("session_end", self.on_end_session) - asyncio.async(self.accept_routine()) + asyncio.ensure_future(self.accept_routine()) self.scenario = scenario self.biboumi = biboumi @@ -74,7 +76,7 @@ class XMPPComponent(slixmpp.BaseXMPP): self.scenario.steps = [] self.failed = True - def on_end_session(self, event): + def on_end_session(self, _): self.loop.stop() def handle_incoming_stanza(self, stanza): @@ -91,16 +93,17 @@ class XMPPComponent(slixmpp.BaseXMPP): self.run_scenario() def run_scenario(self): - if scenario.steps: - step = scenario.steps.pop(0) + if self.scenario.steps: + step = self.scenario.steps.pop(0) step(self, self.biboumi) else: - self.biboumi.stop() + if self.biboumi: + self.biboumi.stop() @asyncio.coroutine def accept_routine(self): self.accepting_server = yield from self.loop.create_server(lambda: self, - "127.0.0.1", "8811", reuse_address=True) + "127.0.0.1", 8811, reuse_address=True) def check_stanza_against_all_expected_xpaths(self): pass @@ -110,15 +113,26 @@ 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': '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'}) + 'version': 'jabber:iq:version', + 'mam': 'urn:xmpp:mam:1', + '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'}) return matched def check_xpath(xpaths, xmpp, after, stanza): - for i, xpath in enumerate(xpaths): + for xpath in xpaths: matched = match(stanza, xpath) if not matched: raise StanzaError("Received stanza “%s” did not match expected xpath “%s”" % (stanza, xpath)) @@ -129,6 +143,28 @@ def check_xpath(xpaths, xmpp, after, stanza): 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: @@ -187,10 +223,11 @@ class ProcessRunner: class BiboumiRunner(ProcessRunner): - def __init__(self, name, with_valgrind): + def __init__(self, name): super().__init__() self.name = name self.fd = open("biboumi_%s_output.txt" % (name,), "w") + with_valgrind = os.environ.get("E2E_WITH_VALGRIND") is not None if with_valgrind: self.create = asyncio.create_subprocess_exec("valgrind", "--suppressions=" + (os.environ.get("E2E_BIBOUMI_SUPP_DIR") or "") + "biboumi.supp", "--leak-check=full", "--show-leak-kinds=all", "--errors-for-leak-kinds=all", "--error-exitcode=16", @@ -224,6 +261,19 @@ def expect_stanza(xpaths, xmpp, biboumi, optional=False, after=None): else: print("Warning, from argument type passed to expect_stanza: %s" % (type(xpaths))) +# 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) def log_message(message, xmpp, biboumi): print("[33;1m%s[0m" % (message,)) @@ -241,7 +291,8 @@ class BiboumiTest: self.scenario = scenario self.expected_code = expected_code - def run(self, with_valgrind=True): + def run(self): + with_valgrind = os.environ.get("E2E_WITH_VALGRIND") is not None print("Running scenario: [33;1m%s[0m%s" % (self.scenario.name, " (with valgrind)" if with_valgrind else '')) # Redirect the slixmpp logging into a specific file output_filename = "slixmpp_%s_output.txt" % (self.scenario.name,) @@ -252,10 +303,15 @@ class BiboumiTest: filename=output_filename) with open("test.conf", "w") as fd: - fd.write(confs[scenario.conf]) + fd.write(confs[self.scenario.conf]) + + try: + os.remove("e2e_test.sqlite") + except FileNotFoundError: + pass # Start the XMPP component and biboumi - biboumi = BiboumiRunner(scenario.name, with_valgrind) + biboumi = BiboumiRunner(self.scenario.name) xmpp = XMPPComponent(self.scenario, biboumi) asyncio.get_event_loop().run_until_complete(biboumi.start()) @@ -264,7 +320,7 @@ class BiboumiTest: xmpp.process() code = asyncio.get_event_loop().run_until_complete(biboumi.wait()) xmpp.biboumi = None - scenario.steps.clear() + self.scenario.steps.clear() failed = False if not xmpp.failed: if code != self.expected_code: @@ -311,6 +367,7 @@ common_replacements = { 'jid_two': 'second@example.com', 'jid_admin': 'admin@example.com', 'nick_two': 'Bobby', + 'nick_three': 'Bernard', 'lower_nick_one': 'nick', 'lower_nick_two': 'bobby', } @@ -428,7 +485,24 @@ if __name__ == '__main__': ), partial(expect_stanza, "/message[@from='#foo%{irc_server_one}'][@type='groupchat']/subject[not(text())]"), ]), - Scenario("virtual_channel_join", + Scenario("virtual_channel", + [ + handshake_sequence(), + partial(send_stanza, + "<presence from='{jid_one}/{resource_one}' to='%{irc_server_one}/{nick_one}' />"), + connection_begin_sequence("irc.localhost", '{jid_one}/{resource_one}'), + partial(expect_stanza, + ("/presence[@to='{jid_one}/{resource_one}'][@from='%{irc_server_one}/{nick_one}']/muc_user:x/muc_user:item[@affiliation='none'][@role='participant']", + "/presence/muc_user:x/muc_user:status[@code='110']") + ), + partial(expect_stanza, "/message[@from='%{irc_server_one}'][@type='groupchat']/subject[re:test(text(), '^This is a virtual channel.*$')]"), + connection_end_sequence("irc.localhost", '{jid_one}/{resource_one}'), + partial(send_stanza, "<presence type='unavailable' from='{jid_one}/{resource_one}' to='%{irc_server_one}/{nick_one}' />"), + partial(expect_stanza, "/presence[@type='unavailable'][@from='%{irc_server_one}/{nick_one}']"), + partial(expect_stanza, "/message[@from='{irc_server_one}']/body[text()='ERROR: Closing Link: localhost (Client Quit)']"), + partial(expect_stanza, "/message[@from='{irc_server_one}']/body[text()='ERROR: Connection closed.']"), + ]), + Scenario("irc_server_disconnection", [ handshake_sequence(), partial(send_stanza, @@ -440,6 +514,21 @@ if __name__ == '__main__': ), partial(expect_stanza, "/message[@from='%{irc_server_one}'][@type='groupchat']/subject[re:test(text(), '^This is a virtual channel.*$')]"), connection_end_sequence("irc.localhost", '{jid_one}/{resource_one}'), + + partial(send_stanza, "<presence from='{jid_one}/{resource_one}' to='%{irc_server_one}/{nick_two}' />"), + + partial(expect_unordered, [ + ("/presence[@from='%{irc_server_one}/{nick_one}'][@to='{jid_one}/{resource_one}'][@type='unavailable']/muc_user:x/muc_user:item[@nick='{nick_two}']", + "/presence/muc_user:x/muc_user:status[@code='110']", + "/presence/muc_user:x/muc_user:status[@code='303']"), + ("/presence[@from='%{irc_server_one}/{nick_two}'][@to='{jid_one}/{resource_one}']", + "/presence/muc_user:x/muc_user:status[@code='110']"), + ]), + + partial(send_stanza, "<presence type='unavailable' from='{jid_one}/{resource_one}' to='%{irc_server_one}/{nick_two}' />"), + partial(expect_stanza, "/presence[@type='unavailable'][@from='%{irc_server_one}/{nick_two}']"), + partial(expect_stanza, "/message[@from='{irc_server_one}']/body[text()='ERROR: Closing Link: localhost (Client Quit)']"), + partial(expect_stanza, "/message[@from='{irc_server_one}']/body[text()='ERROR: Connection closed.']"), ]), Scenario("channel_join_with_two_users", [ @@ -464,24 +553,13 @@ if __name__ == '__main__': 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(log_message, - "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(log_message, - "The other user presence"), - partial(expect_stanza, - "/presence[@to='{jid_second}/{resource_one}'][@from='#foo%{irc_server_one}/{nick_one}']/muc_user:x/muc_user:item[@affiliation='none'][@jid='~nick@localhost'][@role='participant']"), - # Our own presence - partial(log_message, - "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}'][@type='groupchat']/subject[not(text())]"), + 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", [ @@ -512,7 +590,7 @@ if __name__ == '__main__': ("/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_second}/{resource_one}'][@from='#foo%{irc_server_one}/{nick_one}']/muc_user:x/muc_user:item[@affiliation='admin'][@jid='~nick@localhost'][@role='moderator']"), + "/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']", @@ -554,7 +632,7 @@ if __name__ == '__main__': handshake_sequence(), partial(send_stanza, "<iq type='get' id='idwhatever' from='{jid_one}/{resource_one}' to='{biboumi_host}'><query xmlns='http://jabber.org/protocol/disco#items' node='http://jabber.org/protocol/commands' /></iq>"), partial(expect_stanza, ("/iq[@type='result']/disco_items:query[@node='http://jabber.org/protocol/commands']", - "/iq/disco_items:query/disco_items:item[3]")), + "/iq/disco_items:query/disco_items:item[5]")), ], conf='fixed_server'), Scenario("list_admin_adhoc_fixed_server", [ @@ -570,7 +648,7 @@ if __name__ == '__main__': 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[1]")), + "/iq/disco_items:query/disco_items:item[2]")), ]), Scenario("list_adhoc_irc_fixed_server", [ @@ -603,6 +681,35 @@ if __name__ == '__main__': 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_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(log_message, "Join a channel"), + 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("multisessionnick", [ handshake_sequence(), @@ -621,51 +728,85 @@ if __name__ == '__main__': partial(send_stanza, "<presence from='{jid_one}/{resource_two}' to='#foo%{irc_server_one}/{nick_one}' />"), # We receive our own join - 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_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())]",)] ), - partial(expect_stanza, "/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_stanza, - ("/presence[@to='{jid_one}/{resource_one}'][@from='#foo%{irc_server_one}/{nick_two}']",)), - partial(expect_stanza, - ("/presence[@to='{jid_one}/{resource_two}'][@from='#foo%{irc_server_one}/{nick_two}']",)), + 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, - "/presence[@to='{jid_second}/{resource_one}'][@from='#foo%{irc_server_one}/{nick_one}']"), - partial(expect_stanza, - ("/presence[@to='{jid_two}/{resource_one}'][@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']/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']"), - partial(expect_stanza, "/message[@from='{lower_nick_two}!{irc_server_one}'][@to='{jid_one}/{resource_two}'][@type='chat']/body[text()='RELLO']"), + 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")), + partial(expect_stanza, "/message[@from='{lower_nick_two}%{irc_server_one}'][@to='{jid_one}/{resource_two}'][@type='chat']/body[text()='RELLO']"), + + + partial(log_message, "Nickname conflict"), + # 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']",), + ]), + + # partial(log_message, "Nickname change"), + # 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.']") + "/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_one}' type='chat'><body>first</body></message>"), - partial(send_stanza, "<message from='{jid_two}/{resource_one}' to='#foo%{irc_server_one}/{nick_one}' type='chat'><body>second</body></message>"), + 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_stanza, "/message[@from='{lower_nick_two}!{irc_server_one}'][@to='{jid_one}/{resource_one}'][@type='chat']/body[text()='first']"), - partial(expect_stanza, "/message[@from='{lower_nick_two}!{irc_server_one}'][@to='{jid_one}/{resource_one}'][@type='chat']/body[text()='second']"), - + 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("channel_messages", [ @@ -677,7 +818,7 @@ if __name__ == '__main__': partial(expect_stanza, "/message/body[text()='Mode #foo [+nt] by {irc_host_one}']"), partial(expect_stanza, - ("/presence[@to='{jid_one}/{resource_one}'][@from='#foo%{irc_server_one}/{nick_one}']/muc_user:x/muc_user:item[@affiliation='admin'][@jid='~nick@localhost'][@role='moderator']", + ("/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())]"), @@ -687,31 +828,29 @@ if __name__ == '__main__': "<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_second}/{resource_one}'][@from='#foo%{irc_server_one}/{nick_one}']/muc_user:x/muc_user:item[@affiliation='none'][@jid='~nick@localhost'][@role='participant']"), - # 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}'][@type='groupchat']/subject[not(text())]"), + 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_stanza, "/message[@from='#foo%{irc_server_one}/{nick_one}'][@to='{jid_one}/{resource_one}'][@type='groupchat']/body[text()='coucou']"), - partial(expect_stanza, "/message[@from='#foo%{irc_server_one}/{nick_one}'][@to='{jid_two}/{resource_one}'][@type='groupchat']/body[text()='coucou']"), + partial(expect_unordered, [ + ("/message[@from='#foo%{irc_server_one}/{nick_one}'][@to='{jid_one}/{resource_one}'][@type='groupchat']/body[text()='coucou']",), + ("/message[@from='#foo%{irc_server_one}/{nick_one}'][@to='{jid_two}/{resource_one}'][@type='groupchat']/body[text()='coucou']",) + ]), # 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']"), + 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>"), + 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']"), @@ -729,10 +868,10 @@ if __name__ == '__main__': # Send a private message, to a in-room JID partial(send_stanza, "<message from='{jid_one}/{resource_one}' to='%{irc_server_one}/{nick_two}' type='chat'><body>re in private</body></message>"), # 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']"), + 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>"), + partial(send_stanza, "<message from='{jid_two}/{resource_one}' to='{lower_nick_one}%{irc_server_one}' type='chat'><body>re</body></message>"), # The response is received from the in-room JID partial(expect_stanza, "/message[@from='%{irc_server_one}/{nick_two}'][@to='{jid_one}/{resource_one}'][@type='chat']/body[text()='re']"), @@ -743,9 +882,9 @@ if __name__ == '__main__': "/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(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}']"), + "/message[@from='{lower_nick_two}%{irc_server_one}'][@to='{jid_one}/{resource_one}']"), ] ), Scenario("encoded_channel_join", @@ -781,10 +920,10 @@ if __name__ == '__main__': "<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']"), + "/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}'/>"), + "<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']"), @@ -802,21 +941,21 @@ if __name__ == '__main__': "<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']", + "/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}'/>"), + "<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']", + "/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}'/>"), + "<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']"), @@ -836,34 +975,30 @@ if __name__ == '__main__': 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, - "/presence/muc_user:x/muc_user:item[@affiliation='none'][@role='participant']",), - - partial(expect_stanza, - "/presence/muc_user:x/muc_user:item[@affiliation='admin'][@role='moderator']"), - partial(expect_stanza, - "/presence/muc_user:x/muc_user:status[@code='110']"), - partial(expect_stanza, "/message/subject"), + 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(log_message, "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(log_message, "Presence is sent to everyone"), - partial(expect_stanza, - ("/presence[@type='unavailable'][@to='{jid_second}/{resource_one}']/muc_user:x/muc_user:item[@role='none']/muc_user:actor[@nick='{nick_one}']", + 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']" - )), - partial(expect_stanza, - ("/presence[@type='unavailable']/muc_user:x/muc_user:item[@role='none']/muc_user:actor[@nick='{nick_one}']", + ), + ("/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']", ), - ), - partial(expect_stanza, - "/iq[@id='kick1'][@type='result']"), + ("/iq[@id='kick1'][@type='result']",), + ]), ]), Scenario("multisession_kick", [ @@ -876,18 +1011,16 @@ if __name__ == '__main__': partial(expect_stanza, "/presence/muc_user:x/muc_user:status[@code='110']"), partial(expect_stanza, "/message[@type='groupchat']/subject"), - # Second user joins, from two resources + # 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_stanza, - "/presence/muc_user:x/muc_user:item[@affiliation='none'][@role='participant']",), - - partial(expect_stanza, - "/presence/muc_user:x/muc_user:item[@affiliation='admin'][@role='moderator']"), - partial(expect_stanza, - "/presence/muc_user:x/muc_user:status[@code='110']"), - partial(expect_stanza, "/message/subject"), + 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}' />"), @@ -904,26 +1037,23 @@ if __name__ == '__main__': 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(log_message, "Unavailable presence is sent to the two resources"), - partial(expect_stanza, - ("/presence[@type='unavailable'][@to='{jid_second}/{resource_one}']/muc_user:x/muc_user:item[@role='none']/muc_user:actor[@nick='{nick_one}']", + 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']" - )), - partial(expect_stanza, - ("/presence[@type='unavailable'][@to='{jid_second}/{resource_two}']/muc_user:x/muc_user:item[@role='none']/muc_user:actor[@nick='{nick_one}']", + ), + ("/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']" - )), - partial(expect_stanza, + ), ("/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']", ), - ), - partial(expect_stanza, - "/iq[@id='kick1'][@type='result']"), + ("/iq[@id='kick1'][@type='result']",), + ]), ]), Scenario("self_version", [ @@ -944,11 +1074,11 @@ if __name__ == '__main__': "<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}']", + "/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>"), + "<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']"), @@ -966,12 +1096,12 @@ if __name__ == '__main__': "<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]", + "/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>"), + "<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']"), @@ -980,19 +1110,755 @@ if __name__ == '__main__': "<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]", + "/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>"), + "<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", + [ + partial(log_message, "Joining 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']/subject[not(text())]"), + + partial(log_message, "Send a version request to ourself"), + 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(log_message, "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'))), + partial(log_message, "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='{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']"), + + 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:1' queryid='qid1' /></iq>"), + + partial(expect_stanza, + ("/message/mam:result[@queryid='qid1']/forward:forwarded/delay:delay", + "/message/mam:result[@queryid='qid1']/forward:forwarded/client:message[@from='#foo%{irc_server_one}/{nick_one}'][@type='groupchat']/client:body[text()='coucou']") + ), + partial(expect_stanza, + ("/message/mam:result[@queryid='qid1']/forward:forwarded/delay:delay", + "/message/mam:result[@queryid='qid1']/forward:forwarded/client:message[@from='#foo%{irc_server_one}/{nick_one}'][@type='groupchat']/client:body[text()='coucou 2']") + ), + + partial(expect_stanza, + "/iq[@type='result'][@id='id1'][@from='#foo%{irc_server_one}'][@to='{jid_one}/{resource_one}']"), + + # 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:1' queryid='qid2'> + <x xmlns='jabber:x:data' type='submit'> + <field var='FORM_TYPE' type='hidden'> <value>urn:xmpp:mam:1</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}']"), + + # 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:1' queryid='qid3'> + <x xmlns='jabber:x:data' type='submit'> + <field var='FORM_TYPE' type='hidden'> <value>urn:xmpp:mam:1</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}']"), + ]), + 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}'), + 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:1' queryid='qid1' /></iq>"), + + partial(expect_stanza, + ("/message/mam:result[@queryid='qid1']/forward:forwarded/delay:delay", + "/message/mam:result[@queryid='qid1']/forward:forwarded/client:message[@from='#foo@{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("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}'), + 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}' />"), + # connection_sequence("irc.localhost", '{jid_one}/{resource_two}'), + # partial(expect_stanza, + # "/message/body[text()='Mode #foo [+nt] by {irc_host_one}']"), + partial(expect_stanza, + ("/presence[@to='{jid_one}/{resource_two}'][@from='#foo%{irc_server_one}/{nick_one}']/muc_user:x/muc_user:item[@affiliation='admin'][@jid='~nick@localhost'][@role='moderator']", + "/presence/muc_user:x/muc_user:status[@code='110']") + ), + # 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(log_message, "Join first channel #foo"), + 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(log_message, "Join second channel #bar"), + 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(log_message, "Request the whole channel list"), + 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_with_rsm", + [ + handshake_sequence(), + + partial(log_message, "Join first channel #foo"), + 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(log_message, "Join second channel #bar"), + 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(log_message, "Join third channel #coucou"), + 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())]"), + + partial(log_message, "Request with max=0"), + 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>"), + partial(expect_stanza, ( + "/iq[@type='result']/disco_items:query", + )), + + partial(log_message, "Request with max=2"), + 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}']", + "/iq/disco_items:query/rsm:set/rsm:count[text()='3']" + )), + + partial(log_message, "Request with max=12"), + 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']" + )), + + partial(log_message, "Request with max=1 after=#bar"), + 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']" + )), + + partial(log_message, "Request with max=1 after=#bar"), + 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']" + )) + ]), + Scenario("complete_channel_list_with_pages_of_3", + [ + handshake_sequence(), + + partial(log_message, "Join 10 channels"), + 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(log_message, "Request the first page, with a limit of 3"), + 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(log_message, "Request subsequent pages"), + 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(log_message, "Leaving the 10 channels"), + 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[@type='result']/disco_info:query[@node='http://jabber.org/protocol/muc#traffic']"), + ]), + 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("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:1']", + "/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}']"), + ]), + Scenario("virtual_channel_multisession", + [ + handshake_sequence(), + partial(send_stanza, + "<presence from='{jid_one}/{resource_one}' to='%{irc_server_one}/{nick_one}' />"), + connection_begin_sequence("irc.localhost", '{jid_one}/{resource_one}'), + partial(expect_stanza, + ("/presence[@to='{jid_one}/{resource_one}'][@from='%{irc_server_one}/{nick_one}']/muc_user:x/muc_user:item[@affiliation='none'][@role='participant']", + "/presence/muc_user:x/muc_user:status[@code='110']") + ), + partial(expect_stanza, "/message[@from='%{irc_server_one}'][@type='groupchat']/subject[re:test(text(), '^This is a virtual channel.*$')]"), + connection_end_sequence("irc.localhost", '{jid_one}/{resource_one}'), + + partial(send_stanza, + "<presence from='{jid_one}/{resource_two}' to='%{irc_server_one}/{nick_one}' />"), + + partial(expect_stanza, + ("/presence[@to='{jid_one}/{resource_two}'][@from='%{irc_server_one}/{nick_one}']/muc_user:x/muc_user:item[@affiliation='none'][@role='participant']", + "/presence/muc_user:x/muc_user:status[@code='110']") + ), + partial(expect_stanza, "/message[@to='{jid_one}/{resource_two}'][@from='%{irc_server_one}'][@type='groupchat']/subject[re:test(text(), '^This is a virtual channel.*$')]"), + + + partial(log_message, "Nick change"), + partial(send_stanza, "<presence from='{jid_one}/{resource_one}' to='%{irc_server_one}/{nick_two}' />"), + + partial(expect_unordered, [ + ("/presence[@from='%{irc_server_one}/{nick_one}'][@to='{jid_one}/{resource_two}'][@type='unavailable']/muc_user:x/muc_user:item[@nick='Bobby']", + "/presence/muc_user:x/muc_user:status[@code='303']", + "/presence/muc_user:x/muc_user:status[@code='110']"), + ("/presence[@from='%{irc_server_one}/{nick_two}'][@to='{jid_one}/{resource_two}']", + "/presence/muc_user:x/muc_user:status[@code='110']"), + + ("/presence[@from='%{irc_server_one}/{nick_one}'][@to='{jid_one}/{resource_one}'][@type='unavailable']/muc_user:x/muc_user:item[@nick='Bobby']", + "/presence/muc_user:x/muc_user:status[@code='303']", + "/presence/muc_user:x/muc_user:status[@code='110']"), + ("/presence[@from='%{irc_server_one}/{nick_two}'][@to='{jid_one}/{resource_one}']", + "/presence/muc_user:x/muc_user:status[@code='110']"), + ]), + + + partial(log_message, "First resource leaves."), + partial(send_stanza, "<presence type='unavailable' from='{jid_one}/{resource_one}' to='%{irc_server_one}/{nick_two}' />"), + partial(expect_stanza, ("/presence[@type='unavailable'][@from='%{irc_server_one}/{nick_two}'][@to='{jid_one}/{resource_one}']/muc_user:x/muc_user:status[@code='110']", + "/presence/status[text()='Biboumi note: 1 resources are still in this channel.']",) + ), + + partial(log_message, "Second resource leaves."), + partial(send_stanza, "<presence type='unavailable' from='{jid_one}/{resource_two}' to='%{irc_server_one}/{nick_two}' />"), + partial(expect_stanza, "/presence[@type='unavailable'][@from='%{irc_server_one}/{nick_two}']"), + partial(expect_stanza, "/message[@from='{irc_server_one}']/body[text()='ERROR: Closing Link: localhost (Client Quit)']"), + partial(expect_stanza, "/message[@from='{irc_server_one}']/body[text()='ERROR: Connection closed.']"), + ]), + 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/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/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("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-single'][@var='after_connect_command']", + "/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']/dataform:value[text()='ISO-8859-1']", + "/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_command'><value>INVALID command</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='after_connect_command']/dataform:value[text()='INVALID 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']"), + ]), + Scenario("get_irc_connection_info", + [ + handshake_sequence(), + + partial(log_message, "Not connected yet"), + 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(log_message, "Join one room"), + 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(log_message, "Not connected yet"), + 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(log_message, "Join one room"), + partial(send_stanza, + "<presence from='{jid_one}/{resource_one}' to='#foo@{biboumi_host}/{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='{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("invalid_room_jid", + [ + handshake_sequence(), + partial(send_stanza, "<presence from='{jid_one}/{resource_one}' to='invalid%{irc_server_one}/{nick_one}' />"), + partial(expect_stanza, ("/presence[@type='error'][@to='{jid_one}/{resource_one}'][@from='invalid%{irc_server_one}/{nick_one}']/error[@type='cancel']/stanza:item-not-found", + "/presence/muc:x", + "/presence/error/stanza:text")), + ]), + Scenario("invalid_room_jid_fixed", + [ + handshake_sequence(), + partial(send_stanza, "<presence from='{jid_one}/{resource_one}' to='invalid@{biboumi_host}/{nick_one}' />"), + partial(expect_stanza, ("/presence[@type='error'][@to='{jid_one}/{resource_one}'][@from='invalid@{biboumi_host}/{nick_one}']/error[@type='cancel']/stanza:item-not-found", + "/presence/muc:x", + "/presence/error/stanza:text")), + ], conf='fixed_server'), ) + failures = 0 + scenar_list = sys.argv[1:] irc_output = open("irc_output.txt", "w") irc = IrcServerRunner() print("Starting irc server…") @@ -1008,11 +1874,13 @@ if __name__ == '__main__': print("irc server started.") print("Running %s checks for biboumi." % (len(scenarios))) - for scenario in scenarios: - test = BiboumiTest(scenario) - if not test.run(os.getenv("E2E_BIBOUMI_VALGRIND") is not None): + 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." % - (scenario.name, scenario.name)) + (s.name, s.name)) failures += 1 print("Waiting for irc server to exit…") diff --git a/tests/end_to_end/ircd.conf b/tests/end_to_end/ircd.conf index 7edb3a8..7327531 100644 --- a/tests/end_to_end/ircd.conf +++ b/tests/end_to_end/ircd.conf @@ -477,8 +477,8 @@ general { operspy_admin_only = no; operspy_dont_care_user_info = no; caller_id_wait = 1 minute; - pace_wait_simple = 1 second; - pace_wait = 10 seconds; + pace_wait_simple = 0 second; + pace_wait = 0 seconds; short_motd = no; ping_cookie = no; connect_timeout = 30 seconds; diff --git a/tests/iid.cpp b/tests/iid.cpp index 74d010d..b42b9e5 100644 --- a/tests/iid.cpp +++ b/tests/iid.cpp @@ -47,66 +47,60 @@ namespace Catch TEST_CASE("Iid creation") { - Iid iid1("foo!irc.example.org"); - CHECK(std::to_string(iid1) == "foo!irc.example.org"); + const std::set<char> chantypes {'#', '&'}; + Iid iid1("foo%irc.example.org", chantypes); + CHECK(std::to_string(iid1) == "foo%irc.example.org"); CHECK(iid1.get_local() == "foo"); CHECK(iid1.get_server() == "irc.example.org"); - CHECK(!iid1.is_channel); - CHECK(iid1.is_user); + CHECK(iid1.type == Iid::Type::User); - Iid iid2("#test%irc.example.org"); + Iid iid2("#test%irc.example.org", chantypes); CHECK(std::to_string(iid2) == "#test%irc.example.org"); CHECK(iid2.get_local() == "#test"); CHECK(iid2.get_server() == "irc.example.org"); - CHECK(iid2.is_channel); - CHECK(!iid2.is_user); + CHECK(iid2.type == Iid::Type::Channel); - Iid iid3("%irc.example.org"); + Iid iid3("%irc.example.org", chantypes); CHECK(std::to_string(iid3) == "%irc.example.org"); - CHECK(iid3.get_local() == ""); + CHECK(iid3.get_local().empty()); CHECK(iid3.get_server() == "irc.example.org"); - CHECK(iid3.is_channel); - CHECK(!iid3.is_user); + CHECK(iid3.type == Iid::Type::Channel); - Iid iid4("irc.example.org"); + Iid iid4("irc.example.org", chantypes); CHECK(std::to_string(iid4) == "irc.example.org"); CHECK(iid4.get_local() == ""); CHECK(iid4.get_server() == "irc.example.org"); - CHECK(!iid4.is_channel); - CHECK(!iid4.is_user); + CHECK(iid4.type == Iid::Type::Server); - Iid iid5("nick!"); - CHECK(std::to_string(iid5) == "nick!"); + Iid iid5("nick%", chantypes); + CHECK(std::to_string(iid5) == "nick%"); CHECK(iid5.get_local() == "nick"); CHECK(iid5.get_server() == ""); - CHECK(!iid5.is_channel); - CHECK(iid5.is_user); + CHECK(iid5.type == Iid::Type::User); - Iid iid6("##channel%"); + Iid iid6("##channel%", chantypes); CHECK(std::to_string(iid6) == "##channel%"); CHECK(iid6.get_local() == "##channel"); CHECK(iid6.get_server() == ""); - CHECK(iid6.is_channel); - CHECK(!iid6.is_user); + CHECK(iid6.type == Iid::Type::Channel); } TEST_CASE("Iid creation in fixed_server mode") { Config::set("fixed_irc_server", "fixed.example.com", false); - Iid iid1("foo!irc.example.org"); - CHECK(std::to_string(iid1) == "foo!"); - CHECK(iid1.get_local() == "foo"); + const std::set<char> chantypes {'#', '&'}; + Iid iid1("foo%irc.example.org", chantypes); + CHECK(std::to_string(iid1) == "foo%irc.example.org"); + CHECK(iid1.get_local() == "foo%irc.example.org"); CHECK(iid1.get_server() == "fixed.example.com"); - CHECK(!iid1.is_channel); - CHECK(iid1.is_user); + CHECK(iid1.type == Iid::Type::User); - Iid iid2("#test%irc.example.org"); + Iid iid2("#test%irc.example.org", chantypes); CHECK(std::to_string(iid2) == "#test%irc.example.org"); CHECK(iid2.get_local() == "#test%irc.example.org"); CHECK(iid2.get_server() == "fixed.example.com"); - CHECK(iid2.is_channel); - CHECK(!iid2.is_user); + CHECK(iid2.type == Iid::Type::Channel); // Note that it is impossible to adress the IRC server directly, or to // use the virtual channel, in that mode @@ -114,17 +108,15 @@ TEST_CASE("Iid creation in fixed_server mode") // Iid iid3("%irc.example.org"); // Iid iid4("irc.example.org"); - Iid iid5("nick!"); - CHECK(std::to_string(iid5) == "nick!"); + Iid iid5("nick", chantypes); + CHECK(std::to_string(iid5) == "nick"); CHECK(iid5.get_local() == "nick"); CHECK(iid5.get_server() == "fixed.example.com"); - CHECK(!iid5.is_channel); - CHECK(iid5.is_user); + CHECK(iid5.type == Iid::Type::User); - Iid iid6("##channel%"); + Iid iid6("##channel%", chantypes); CHECK(std::to_string(iid6) == "##channel%"); CHECK(iid6.get_local() == "##channel%"); CHECK(iid6.get_server() == "fixed.example.com"); - CHECK(iid6.is_channel); - CHECK(!iid6.is_user); + CHECK(iid6.type == Iid::Type::Channel); } diff --git a/tests/timed_events.cpp b/tests/timed_events.cpp index d63abef..fece422 100644 --- a/tests/timed_events.cpp +++ b/tests/timed_events.cpp @@ -38,7 +38,6 @@ TEST_CASE("Test timed event expiration") CHECK(TimedEventsManager::instance().execute_expired_events() == 0); std::chrono::milliseconds timoute = TimedEventsManager::instance().get_timeout(); - INFO("Sleeping for " << timoute.count() << "ms"); std::this_thread::sleep_for(timoute + 1ms); // Event is now expired diff --git a/tests/utils.cpp b/tests/utils.cpp index 01d070e..48951da 100644 --- a/tests/utils.cpp +++ b/tests/utils.cpp @@ -6,6 +6,10 @@ #include <utils/split.hpp> #include <utils/xdg.hpp> #include <utils/empty_if_fixed_server.hpp> +#include <utils/get_first_non_empty.hpp> +#include <utils/time.hpp> + +using namespace std::string_literals; TEST_CASE("String split") { @@ -100,3 +104,39 @@ TEST_CASE("string cut") CHECK(res[0] == "rhello, "); CHECK(res[1] == "♥"); } + +TEST_CASE("first non-empty string") +{ + CHECK(get_first_non_empty(""s, ""s, "hello"s, "world"s) == "hello"s); + CHECK(get_first_non_empty(""s, ""s, ""s, ""s) == ""s); + CHECK(get_first_non_empty("first"s) == "first"s); + CHECK(get_first_non_empty(0, 1, 2, 3) == 1); +} + +TEST_CASE("time_to_string") +{ + const std::time_t stamp = 1472480968; + const std::string result = "2016-08-29T14:29:28Z"; + CHECK(utils::to_string(stamp) == result); + CHECK(utils::to_string(stamp).size() == result.size()); + CHECK(utils::to_string(0) == "1970-01-01T00:00:00Z"); +} + +TEST_CASE("parse_datetime") +{ + CHECK(utils::parse_datetime("1970-01-01T00:00:00Z") == 0); + + const int twenty_three_hours = 82800; + CHECK(utils::parse_datetime("1970-01-01T23:00:12Z") == twenty_three_hours + 12); + CHECK(utils::parse_datetime("1970-01-01T23:00:12Z") == utils::parse_datetime("1970-01-01T23:00:12+00:00")); + CHECK(utils::parse_datetime("1970-01-01T23:00:12Z") == utils::parse_datetime("1970-01-01T23:00:12-00:00")); + CHECK(utils::parse_datetime("1970-01-02T00:00:12Z") == utils::parse_datetime("1970-01-01T23:00:12-01:00")); + CHECK(utils::parse_datetime("1970-01-02T00:00:12Z") == utils::parse_datetime("1970-01-02T01:00:12+01:00")); + + CHECK(utils::parse_datetime("2016-08-29T14:29:29Z") == 1472480969); + + CHECK(utils::parse_datetime("blah") == -1); + CHECK(utils::parse_datetime("1970-01-02T00:00:12B") == -1); + CHECK(utils::parse_datetime("1970-01-02T00:00:12*00:00") == -1); + CHECK(utils::parse_datetime("1970-01-02T00:00:12+0000") == -1); +} diff --git a/tests/xmpp.cpp b/tests/xmpp.cpp index 6aab8c4..37d2b54 100644 --- a/tests/xmpp.cpp +++ b/tests/xmpp.cpp @@ -1,6 +1,7 @@ #include "catch.hpp" #include <xmpp/xmpp_parser.hpp> +#include <xmpp/auth.hpp> TEST_CASE("Test basic XML parsing") { @@ -45,3 +46,9 @@ TEST_CASE("XML escape") const std::string unescaped = "'coucou'<cc>/&\"gaga\""; CHECK(xml_escape(unescaped) == "'coucou'<cc>/&"gaga""); } + +TEST_CASE("handshake_digest") +{ + const auto res = get_handshake_digest("id1234", "S4CR3T"); + CHECK(res == "c92901b5d376ad56269914da0cce3aab976847df"); +} |