summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.codecov.yml3
-rw-r--r--.gitlab-ci.yml148
-rw-r--r--CHANGELOG.rst302
-rw-r--r--CMakeLists.txt44
-rw-r--r--INSTALL.rst21
-rw-r--r--README.rst3
-rw-r--r--cmake/Modules/FindPQ.cmake43
-rw-r--r--conf/irc.gimp.org.policy.txt1
-rw-r--r--conf/irc.gnome.org.policy.txt1
-rw-r--r--conf/irc.ppirc.net.policy.txt1
-rw-r--r--doc/biboumi.1.rst65
-rw-r--r--docker/biboumi-test/alpine/Dockerfile3
-rw-r--r--docker/biboumi-test/debian/Dockerfile5
-rw-r--r--docker/biboumi-test/fedora/Dockerfile1
-rw-r--r--docker/biboumi/alpine/Dockerfile2
-rw-r--r--docker/biboumi/alpine/README.md30
-rw-r--r--docker/packaging/archlinux/Dockerfile (renamed from docker/biboumi-test/archlinux/Dockerfile)0
-rw-r--r--packaging/biboumi.spec.cmake20
-rwxr-xr-xscripts/dump_sqlite3.sh21
-rw-r--r--src/biboumi.h.cmake4
-rw-r--r--src/bridge/bridge.cpp58
-rw-r--r--src/bridge/bridge.hpp7
-rw-r--r--src/bridge/colors.hpp12
-rw-r--r--src/bridge/history_limit.hpp8
-rw-r--r--src/config/config.cpp8
-rw-r--r--src/config/config.hpp1
-rw-r--r--src/database/column.hpp9
-rw-r--r--src/database/count_query.hpp17
-rw-r--r--src/database/database.cpp81
-rw-r--r--src/database/database.hpp73
-rw-r--r--src/database/engine.hpp41
-rw-r--r--src/database/index.hpp38
-rw-r--r--src/database/insert_query.hpp108
-rw-r--r--src/database/postgresql_engine.cpp91
-rw-r--r--src/database/postgresql_engine.hpp48
-rw-r--r--src/database/postgresql_statement.hpp123
-rw-r--r--src/database/query.cpp26
-rw-r--r--src/database/query.hpp62
-rw-r--r--src/database/row.hpp108
-rw-r--r--src/database/select_query.hpp31
-rw-r--r--src/database/sqlite3_engine.cpp101
-rw-r--r--src/database/sqlite3_engine.hpp47
-rw-r--r--src/database/sqlite3_statement.hpp92
-rw-r--r--src/database/statement.hpp46
-rw-r--r--src/database/table.cpp23
-rw-r--r--src/database/table.hpp80
-rw-r--r--src/database/type_to_sql.cpp9
-rw-r--r--src/database/type_to_sql.hpp16
-rw-r--r--src/database/update_query.hpp104
-rw-r--r--src/identd/identd_socket.cpp4
-rw-r--r--src/irc/irc_client.cpp45
-rw-r--r--src/irc/irc_client.hpp7
-rw-r--r--src/logger/logger.cpp23
-rw-r--r--src/logger/logger.hpp87
-rw-r--r--src/main.cpp3
-rw-r--r--src/network/credentials_manager.cpp1
-rw-r--r--src/network/credentials_manager.hpp3
-rw-r--r--src/network/tcp_socket_handler.cpp6
-rw-r--r--src/network/tcp_socket_handler.hpp1
-rw-r--r--src/network/tls_policy.cpp2
-rw-r--r--src/utils/is_one_of.hpp17
-rw-r--r--src/utils/optional_bool.cpp8
-rw-r--r--src/utils/optional_bool.hpp4
-rw-r--r--src/utils/scopetimer.hpp17
-rw-r--r--src/utils/time.cpp3
-rw-r--r--src/utils/time.hpp5
-rw-r--r--src/xmpp/adhoc_commands_handler.cpp2
-rw-r--r--src/xmpp/biboumi_adhoc_commands.cpp10
-rw-r--r--src/xmpp/biboumi_component.cpp101
-rw-r--r--src/xmpp/body.hpp4
-rw-r--r--src/xmpp/xmpp_component.cpp23
-rw-r--r--src/xmpp/xmpp_component.hpp11
-rw-r--r--tests/database.cpp31
-rw-r--r--tests/end_to_end/__main__.py378
-rw-r--r--tests/logger.cpp5
-rw-r--r--tests/utils.cpp12
76 files changed, 2209 insertions, 790 deletions
diff --git a/.codecov.yml b/.codecov.yml
deleted file mode 100644
index 74ba41b..0000000
--- a/.codecov.yml
+++ /dev/null
@@ -1,3 +0,0 @@
-codecov:
- ignore:
- - "tests"
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 6849cb4..a6f7bd8 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -2,8 +2,7 @@ stages:
- build # Build in various conf, keeps the artifacts
- test # Use the build artifacts to run the tests
- packaging # Publish some packages (rpm, deb…)
- - external # Interact with some external service (codecov, coverity…)
- - deploy
+ - external # Interact with some external service (coverity…)
before_script:
- uname -a
@@ -17,6 +16,7 @@ variables:
SYSTEMD: "-DWITH_SYSTEMD=1"
LIBIDN: "-DWITH_LIBIDN=1"
SQLITE3: "-DWITH_SQLITE3=1"
+ POSTGRESQL: "-WITH_POSTGRESQL=1"
#
## Build jobs
@@ -27,10 +27,10 @@ variables:
tags:
- docker
script:
- - "echo Running cmake with the following parameters: -DCMAKE_CXX_COMPILER=${COMPILER} -DCMAKE_BUILD_TYPE=${BUILD_TYPE} ${BOTAN} ${UDNS} ${SYSTEMD} ${LIBIDN} ${SQLITE3}"
+ - "echo Running cmake with the following parameters: -DCMAKE_CXX_COMPILER=${COMPILER} -DCMAKE_BUILD_TYPE=${BUILD_TYPE} ${BOTAN} ${UDNS} ${SYSTEMD} ${LIBIDN} ${SQLITE3} ${POSTGRESQL}"
- mkdir build/
- cd build/
- - cmake .. -DCMAKE_CXX_COMPILER=${COMPILER} -DCMAKE_BUILD_TYPE=${BUILD_TYPE} ${BOTAN} ${UDNS} ${SYSTEMD} ${LIBIDN} ${SQLITE3}
+ - cmake .. -DCMAKE_CXX_FLAGS="-Werror -Wno-psabi" -DCMAKE_CXX_COMPILER=${COMPILER} -DCMAKE_BUILD_TYPE=${BUILD_TYPE} ${BOTAN} ${UDNS} ${SYSTEMD} ${LIBIDN} ${SQLITE3} ${POSTGRESQL}
- make everything -j$(nproc || echo 1)
- make coverage_check -j$(nproc || echo 1)
artifacts:
@@ -42,24 +42,27 @@ variables:
<<: *basic_build
image: docker.louiz.org/louiz/biboumi/test-fedora:latest
-.template:debian_build: &debian_build
+build:fedora:
+ <<: *fedora_build
+
+build:debian:
<<: *basic_build
image: docker.louiz.org/louiz/biboumi/test-debian:latest
-.template:alpine_build: &alpine_build
+build:alpine:
variables:
SYSTEMD: "-DWITHOUT_SYSTEMD=1"
<<: *basic_build
image: docker.louiz.org/louiz/biboumi/test-alpine:latest
-build:fedora:
- <<: *fedora_build
-
-build:debian:
- <<: *debian_build
-
-build:alpine:
- <<: *alpine_build
+build:archlinux:
+ <<: *basic_build
+ only:
+ - branches@louiz/biboumi
+ tags:
+ - armv7l
+ artifacts:
+ paths: []
build:1:
variables:
@@ -74,19 +77,25 @@ build:2:
build:3:
variables:
SQLITE3: "-DWITHOUT_SQLITE3=1"
+ TEST_POSTGRES_URI: "postgres@postgres/postgres"
+ services:
+ - postgres:latest
<<: *fedora_build
build:4:
variables:
SQLITE3: "-DWITHOUT_SQLITE3=1"
+ POSTGRESQL: "-DWITHOUT_POSTGRESQL=1"
BOTAN: "-DWITHOUT_BOTAN=1"
LIBIDN: "-DWITHOUT_LIBIDN=1"
<<: *fedora_build
build:5:
variables:
- SQLITE3: "-DWITHOUT_SQLITE3=1"
UDNS: "-DWITHOUT_UDNS=1"
+ TEST_POSTGRES_URI: "postgres@postgres/postgres"
+ services:
+ - postgres:latest
<<: *fedora_build
build:6:
@@ -95,12 +104,11 @@ build:6:
UDNS: "-DWITHOUT_UDNS=1"
<<: *fedora_build
-build:7:
+build:without_udns:
variables:
UDNS: "-DWITHOUT_UDNS=1"
<<: *fedora_build
-
#
## Test jobs
#
@@ -139,7 +147,7 @@ test:without_udns:
image: docker.louiz.org/louiz/biboumi/test-fedora:latest
<<: *basic_test
dependencies:
- - build:7
+ - build:without_udns
test:alpine:
image: docker.louiz.org/louiz/biboumi/test-alpine:latest
@@ -167,76 +175,6 @@ test:freebsd:
- make check
- make e2e
-#
-## External jobs
-#
-
-.template:codecov: &codecov
- stage: external
- tags:
- - docker
- image: docker.louiz.org/louiz/biboumi/test-fedora:latest
-
-.template:codecov_unittests: &codecov_unittests
- <<: *codecov
- script:
- - bash <(curl -s https://codecov.io/bash) -X gcov -X coveragepy -f ./coverage_test_suite.info -F $(echo $CI_JOB_NAME | sed s/:/_/g | sed s/codecov_//)
-
-.template:codecov_e2e: &codecov_e2e
- <<: *codecov
- script:
- - bash <(curl -s https://codecov.io/bash) -X gcov -X coveragepy -f ./coverage_e2e.info -F $(echo $CI_JOB_NAME | sed s/:/_/g | sed s/codecov_//)
-
-codecov:fedora:
- <<: *codecov_e2e
- dependencies:
- - test:fedora
-
-codecov:without_udns:
- <<: *codecov_e2e
- dependencies:
- - test:without_udns
-
-codecov:debian:
- <<: *codecov_e2e
- dependencies:
- - test:debian
-
-codecov:build:1:
- <<: *codecov_unittests
- dependencies:
- - build:1
-
-codecov:build:2:
- <<: *codecov_unittests
- dependencies:
- - build:2
-
-codecov:build:3:
- <<: *codecov_unittests
- dependencies:
- - build:3
-
-codecov:build:4:
- <<: *codecov_unittests
- dependencies:
- - build:4
-
-codecov:build:5:
- <<: *codecov_unittests
- dependencies:
- - build:5
-
-codecov:build:6:
- <<: *codecov_unittests
- dependencies:
- - build:6
-
-codecov:build:7:
- <<: *codecov_unittests
- dependencies:
- - build:7
-
coverity:
stage: external
only:
@@ -292,6 +230,7 @@ packaging:deb:
before_script: []
script:
- git checkout debian
+ - git pull
- git merge --no-commit --no-ff master
- mk-build-deps
- apt update -y
@@ -312,7 +251,7 @@ packaging:archlinux:
tags:
- docker
allow_failure: true
- image: docker.louiz.org/louiz/biboumi/test-archlinux:latest
+ image: docker.louiz.org/louiz/biboumi/packaging-archlinux:latest
before_script: []
script:
- sudo pacman -Syuu --noconfirm
@@ -321,34 +260,3 @@ packaging:archlinux:
- makepkg -si --noconfirm
- test -e /usr/bin/biboumi
dependencies: []
-
-#
-## Deploy jobs
-#
-
-deploy:docker:
- stage: deploy
- tags:
- - docker-in-docker
- only:
- - master@louiz/biboumi
- variables:
- DOCKER_HOST: tcp://docker.louiz.org:2376
- DOCKER_TLS_VERIFY: 1
- SERVICE_NAME: biboumi
- script:
- - mkdir -p ~/.docker
- - echo "$TLSCACERT" > ~/.docker/ca.pem
- - echo "$TLSCERT" > ~/.docker/cert.pem
- - echo "$TLSKEY" > ~/.docker/key.pem
- - docker version
- - docker login -u gitlab-ci-token -p $CI_JOB_TOKEN docker.louiz.org
- - docker build -t docker.louiz.org/$CI_PROJECT_NAMESPACE/$CI_PROJECT_NAME:$CI_COMMIT_REF_NAME docker/biboumi/alpine
- - docker push docker.louiz.org/$CI_PROJECT_NAMESPACE/$CI_PROJECT_NAME:$CI_COMMIT_REF_NAME
- - docker service create --with-registry-auth --detach=false --name $CI_PROJECT_NAMESPACE-$CI_PROJECT_NAME-1 -e BIBOUMI_PASSWORD=password -e BIBOUMI_XMPP_SERVER_IP=prosody -e BIBOUMI_HOSTNAME=test.biboumi.louiz.org --network xmpp docker.louiz.org/$CI_PROJECT_NAMESPACE/$CI_PROJECT_NAME:$CI_COMMIT_REF_NAME || docker service update $CI_PROJECT_NAMESPACE-$CI_PROJECT_NAME-1
- - docker service create --with-registry-auth --detach=false --name $CI_PROJECT_NAMESPACE-$CI_PROJECT_NAME-2 -e BIBOUMI_PASSWORD=password -e BIBOUMI_XMPP_SERVER_IP=prosody -e BIBOUMI_HOSTNAME=test2.biboumi.louiz.org --network xmpp docker.louiz.org/$CI_PROJECT_NAMESPACE/$CI_PROJECT_NAME:$CI_COMMIT_REF_NAME || docker service update $CI_PROJECT_NAMESPACE-$CI_PROJECT_NAME-2
- - docker service create --with-registry-auth --detach=false --name $CI_PROJECT_NAMESPACE-$CI_PROJECT_NAME-3 -e BIBOUMI_PASSWORD=password -e BIBOUMI_XMPP_SERVER_IP=prosody -e BIBOUMI_HOSTNAME=test2.biboumi.louiz.org --network xmpp docker.louiz.org/$CI_PROJECT_NAMESPACE/$CI_PROJECT_NAME:$CI_COMMIT_REF_NAME || docker service update $CI_PROJECT_NAMESPACE-$CI_PROJECT_NAME-3
- environment:
- name: master
- url: https://biboumi.louiz.org
- dependencies: []
diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index f327fb1..bcddc11 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -1,154 +1,226 @@
-Version 6.0
+Version 8.0
===========
- - The LiteSQL dependency was removed. Only libsqlite3 is now necessary
- to work with the database.
- - Some JIDs can be added into users’ rosters. The component JID tells if
- biboumi is started or not, and the IRC-server JIDs tell if the user is
- currently connected to that server.
- - The RecordHistory option can now also be configured for each IRC channel,
- individually.
- - Add a global option to make all channels persistent.
- - Status code='332' is sent with the unavailable presences when biboumi is
- being shutdown or the connection to the IRC server is cut unexpectedly.
- - Support for botan version 1.11.x has been dropped, only version 2.x is
- supported.
- - Invitations can now be sent to any JID, not only JIDs served by the biboumi
- instance itself.
+- Add a complete='true' in MAM’s iq result when appropriate
+
+Version 7.2 - 2018-01-24
+========================
+
+- Fix compilation with gcc 4.9. This is the last version to support this
+ old version.
+
+Version 7.1 - 2018-01-22
+========================
+
+- Fix a crash happening if a user cancels a non-existing ad-hoc session
+
+Version 7.0 - 2018-01-17
+========================
+
+- Support PostgreSQL as a database backend. See below for migration tips.
+- Add a workaround for a bug in botan < 2.4 where session resumption
+ would sometime result in a TLS decode error
+- Add a <x xmlns="…muc#user"/> node in our private MUC messages, to help
+ clients distinguish between MUC and non-MUC messages.
+- Fix the identd outgoing responses: `\\r\\n` was missing, and some clients
+ would ignore our messages entirely.
+- Fix the iq result sent at the end of a MAM response. Some clients (e.g.
+ gajim) would throw an error as a result.
+- log_level configuration option is no longer ignored if the logs are written
+ into journald
+
+Sqlite3 to PostgreSQL migration
+-------------------------------
+
+If you used biboumi with the sqlite3 database backend and you want to
+start using postgresql instead, follow these simple steps:
+
+- Make sure your Sqlite3 database has the correct format by running at
+ least biboumi version 6.0 against this database. Indeed: biboumi can
+ upgrade your database scheme by itself automatically when it starts, but
+ the migration process can only migrate from the latest known schema,
+ which is the one in version 6.x and 7.x. If you are migrating from
+ version 6.x or 7.x, you have nothing to do.
+- Start biboumi (at least version 7.0) with db_name configured to use
+ your postgresql database: this will create an empty database, create all
+ the tables with all the rights columns, ready to be filled.
+- Backup your database if you value it. The migration process will not
+ write anything into it, so it your data should theorically be kept
+ intact, but we never know.
+- Run the dump script found in biboumi’s sources:
+ `<scripts/dump_sqlite3.sh>`_. Its first and only argument must be the path
+ to your sqlite3 database. For example run `./scripts/dump_sqlite3.sh
+ /var/lib/biboumi/biboumi.sqlite`. This will create, in your current
+ directory, some sqlite files that contain instructions to be fed into
+ postgresql.
+- Import all the ouput files thusly created into your PostgreSQL, with
+ something like this: `psql postgresql://user@password/biboumi < *.sql`.
+ This takes a few minutes if your database is huge (if it contains many
+ archived messages).
+
+Version 6.1 - 2017-10-04
+========================
+
+- Fix compilation with botan 2.3
+- Fix compilation with very old distributions (such as debian wheezy or
+ centos 6) that ship antique softwares (sqlite3 < 3.7.14)
+
+Version 6.0 - 2017-09-17
+========================
+
+- The LiteSQL dependency was removed. Only libsqlite3 is now necessary
+ to work with the database.
+- Some JIDs can be added into users’ rosters. The component JID tells if
+ biboumi is started or not, and the IRC-server JIDs tell if the user is
+ currently connected to that server.
+- The RecordHistory option can now also be configured for each IRC channel,
+ individually.
+- Add a global option to make all channels persistent.
+- The persistent_by_default configuration option has been added, this
+ lets the administrator decide whether or not the rooms should be
+ persistent or not by default, for all users.
+- Status code='332' is sent with the unavailable presences when biboumi is
+ being shutdown or the connection to the IRC server is cut unexpectedly.
+- Support for botan version 1.11.x has been dropped, only version 2.x is
+ supported.
+- Invitations can now be sent to any JID, not only JIDs served by the biboumi
+ instance itself.
+- The history limits sent by the client when they request to join a
+ channel is now supported.
Version 5.0 - 2017-05-24
========================
- - An identd server has been added.
- - Add a **persistent** option for channels. When a channel is configured
- as persistent, when the user leaves the room, biboumi stays idle and keeps
- saving the received messages in the archive, instead of leaving the channel
- entirely. When the user re-joins the room later, biboumi sends the message
- history to her/him. This feature can be used to make biboumi behave like
- an IRC bouncer.
- - Use the udns library instead of c-ares, for asynchronous DNS resolution.
- It’s still fully optional.
- - Update MAM implementation to version 6.0 (namespace mam:2)
- - If the client doesn’t specify any limit in its MAM and channel list request,
- the results returned by biboumi contain at most 100 messages, instead of
- the potentially huge complete result.
- - Multiline topics are now properly handled
- - Configuration options can be overridden by values found in the process env.
- - Botan’s TLS policies can be customized by the administrator, for each
- IRC server, with simple text files.
- - The IRC channel configuration form is now also available using the MUC
- configuration, in addition to the ad-hoc command.
- - Notices starting with [#channel] are considered as welcome messages coming
- from that channel, instead of private messages.
+- An identd server has been added.
+- Add a **persistent** option for channels. When a channel is configured
+ as persistent, when the user leaves the room, biboumi stays idle and keeps
+ saving the received messages in the archive, instead of leaving the channel
+ entirely. When the user re-joins the room later, biboumi sends the message
+ history to her/him. This feature can be used to make biboumi behave like
+ an IRC bouncer.
+- Use the udns library instead of c-ares, for asynchronous DNS resolution.
+ It’s still fully optional.
+- Update MAM implementation to version 6.0 (namespace mam:2)
+- If the client doesn’t specify any limit in its MAM and channel list request,
+ the results returned by biboumi contain at most 100 messages, instead of
+ the potentially huge complete result.
+- Multiline topics are now properly handled
+- Configuration options can be overridden by values found in the process env.
+- Botan’s TLS policies can be customized by the administrator, for each
+ IRC server, with simple text files.
+- The IRC channel configuration form is now also available using the MUC
+ configuration, in addition to the ad-hoc command.
+- Notices starting with [#channel] are considered as welcome messages coming
+ from that channel, instead of private messages.
Version 4.3 - 2017-05-02
========================
- - Fix a segmentation fault that occured when trying to connect to an IRC
- server without any port configured.
+- Fix a segmentation fault that occured when trying to connect to an IRC
+ server without any port configured.
Version 4.2 - 2017-04-26
========================
- - Fix a build issue when LiteSQL is absent from the system
+- Fix a build issue when LiteSQL is absent from the system
Version 4.1 - 2017-03-21
========================
- - Works with botan 2.x, as well as botan 1.11.x
+- Works with botan 2.x, as well as botan 1.11.x
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.
+- 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
========================
- - Support multiple-nick sessions: a user can join an IRC channel behind
- one single nick, using multiple different clients, at the same time (as
- long as each client is using the same bare JID).
- - Database support for persistant per-user per-server configuration. Add
- `LiteSQL <https://dev.louiz.org/projects/litesql>` as an optional
- dependency.
- - Add ad-hoc commands that lets each user configure various things
- - Support an after-connect command that will be sent to the server
- just after the user gets connected to it.
- - Support the sending of a PASS command.
- - Lets the users configure their username and realname, if the
- realname_customization is set to true.
- - The remote TLS certificates are checked against the system’s trusted
- CAs, unless the user used the configuration option that ignores these
- checks.
- - Lets the user set a sha-1 hash to identify a server certificate that
- should always be trusted.
- - Add an outgoing_bind option.
- - Add an ad-hoc command to forcefully disconnect a user from one or
- more servers.
- - Let the user configure the incoming encoding of an IRC server (the
- default behaviour remains unchanged: check if it’s valid utf-8 and if
- not, decode as latin-1).
- - Support `multi-prefix <http://ircv3.net/specs/extensions/multi-prefix-3.1.html>`.
- - And of course, many bufixes.
- - Run unit tests and a test suite, build the RPM and check many things
- automatically using gitlab-ci.
+- Support multiple-nick sessions: a user can join an IRC channel behind
+ one single nick, using multiple different clients, at the same time (as
+ long as each client is using the same bare JID).
+- Database support for persistant per-user per-server configuration. Add
+ `LiteSQL <https://dev.louiz.org/projects/litesql>` as an optional
+ dependency.
+- Add ad-hoc commands that lets each user configure various things
+- Support an after-connect command that will be sent to the server
+ just after the user gets connected to it.
+- Support the sending of a PASS command.
+- Lets the users configure their username and realname, if the
+ realname_customization is set to true.
+- The remote TLS certificates are checked against the system’s trusted
+ CAs, unless the user used the configuration option that ignores these
+ checks.
+- Lets the user set a sha-1 hash to identify a server certificate that
+ should always be trusted.
+- Add an outgoing_bind option.
+- Add an ad-hoc command to forcefully disconnect a user from one or
+ more servers.
+- Let the user configure the incoming encoding of an IRC server (the
+ default behaviour remains unchanged: check if it’s valid utf-8 and if
+ not, decode as latin-1).
+- Support `multi-prefix <http://ircv3.net/specs/extensions/multi-prefix-3.1.html>`.
+- And of course, many bufixes.
+- Run unit tests and a test suite, build the RPM and check many things
+ automatically using gitlab-ci.
Version 2.0 - 2015-05-29
========================
- - List channels on an IRC server through an XMPP disco items request
- - Let the user send any arbitrary raw IRC command by sending a
- message to the IRC server’s JID.
- - By default, look for the configuration file as per the XDG
- basedir spec.
- - Support PING requests in all directions.
- - Improve the way we forward received NOTICEs by remembering to
- which users we previously sent a private message. This improves the
- user experience when talking to NickServ.
- - Support joining key-protected channels
- - Setting a participant's role/affiliation now results in a change of IRC
- mode, instead of being ignored. Setting Toto's affiliation to admin is
- now equivalent to “/mode +o Toto”
- - Fix the reconnection to the XMPP server to try every 2 seconds
- instead of immediately. This avoid hogging resources for nothing
- - Asynchronously resolve domain names by optionally using the DNS
- library c-ares.
- - Add a reload add-hoc command, to reload biboumi's configuration
- - Add a fixed_irc_server option. With this option enabled,
- biboumi can only connect to the one single IRC server configured
+- List channels on an IRC server through an XMPP disco items request
+- Let the user send any arbitrary raw IRC command by sending a
+ message to the IRC server’s JID.
+- By default, look for the configuration file as per the XDG
+ basedir spec.
+- Support PING requests in all directions.
+- Improve the way we forward received NOTICEs by remembering to
+ which users we previously sent a private message. This improves the
+ user experience when talking to NickServ.
+- Support joining key-protected channels
+- Setting a participant's role/affiliation now results in a change of IRC
+ mode, instead of being ignored. Setting Toto's affiliation to admin is
+ now equivalent to “/mode +o Toto”
+- Fix the reconnection to the XMPP server to try every 2 seconds
+ instead of immediately. This avoid hogging resources for nothing
+- Asynchronously resolve domain names by optionally using the DNS
+ library c-ares.
+- Add a reload add-hoc command, to reload biboumi's configuration
+- Add a fixed_irc_server option. With this option enabled,
+ biboumi can only connect to the one single IRC server configured
Version 1.1 - 2014-07-16
========================
- - Fix a segmentation fault when connecting to an IRC server using IPv6
+- Fix a segmentation fault when connecting to an IRC server using IPv6
Version 1.0 - 2014-07-12
========================
- - First stable release.
- - Mostly complete MUC to IRC, and IRC to MUC support
- - Complete handling of private messages
- - Full IRC modes support: setting any IRC mode, and receiving notifications
- for every mode change
- - Verbose connection status notifications
- - Conversion from IRC formatting to XHTML-im
- - Ad-hoc commands support
- - Basic TLS support: auto-accepts all certificates, no cipher
- configuration, no way to force usage of TLS (it is used only if
- available, clear connection is automatically used as a fallback)
- - IPv6 support
+- First stable release.
+- Mostly complete MUC to IRC, and IRC to MUC support
+- Complete handling of private messages
+- Full IRC modes support: setting any IRC mode, and receiving notifications
+ for every mode change
+- Verbose connection status notifications
+- Conversion from IRC formatting to XHTML-im
+- Ad-hoc commands support
+- Basic TLS support: auto-accepts all certificates, no cipher
+ configuration, no way to force usage of TLS (it is used only if
+ available, clear connection is automatically used as a fallback)
+- IPv6 support
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 596d277..2b3f292 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -2,10 +2,20 @@ cmake_minimum_required(VERSION 3.0)
project(biboumi)
-set(${PROJECT_NAME}_VERSION_MAJOR 6)
+set(${PROJECT_NAME}_VERSION_MAJOR 8)
set(${PROJECT_NAME}_VERSION_MINOR 0)
set(${PROJECT_NAME}_VERSION_SUFFIX "~dev")
+if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
+ if (CMAKE_CXX_COMPILER_VERSION VERSION_LESS 5.0)
+ message(FATAL_ERROR "GCC version must be at least 5.0.")
+ endif()
+elseif (CMAKE_CXX_COMPILER_ID STREQUAL "Clang")
+ if (CMAKE_CXX_COMPILER_VERSION VERSION_LESS 3.4)
+ message(FATAL_ERROR "Clang version must be at least 3.4.")
+ endif()
+endif()
+
if(NOT CMAKE_BUILD_TYPE)
set(CMAKE_BUILD_TYPE "Debug" CACHE STRING
"Build type (Release/Debug/RelWithDebInfo/MinSizeRel)" FORCE)
@@ -14,13 +24,13 @@ endif()
#
## Find optional instrumentation libraries that will be used in debug only
#
-find_library(LIBASAN NAMES asan libasan.so.3 libasan.so.2 libasan.so.1)
+find_library(LIBASAN NAMES asan libasan.so.4 libasan.so.3 libasan.so.2 libasan.so.1)
find_library(LIBUBSAN NAMES ubsan libubsan.so.0)
#
## Set various debug flags (instrumentation libs, coverage, …)
#
-set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++1y -pedantic -Wall -Wextra -Wconversion")
+set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++1y -pedantic -Wall -Wextra -Wconversion -fvisibility=hidden -fvisibility-inlines-hidden")
if("${CMAKE_CXX_COMPILER_ID}" STREQUAL "GNU")
set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -fprofile-arcs -ftest-coverage --coverage")
endif()
@@ -130,6 +140,12 @@ elseif(NOT WITHOUT_SQLITE3)
find_package(SQLITE3)
endif()
+if(WITH_POSTGRESQL)
+ find_package(PQ REQUIRED)
+elseif(NOT WITHOUT_POSTGRESQL)
+ find_package(PQ)
+endif()
+
#
## Set all the include directories, depending on what libraries are used
#
@@ -187,12 +203,20 @@ file(GLOB source_network
src/network/*.[hc]pp)
add_library(network OBJECT ${source_network})
-if(SQLITE3_FOUND)
+option(DEBUG_SQL_QUERIES
+ "If set to true, every SQL statement executed will be logged and timed"
+ OFF)
+if(SQLITE3_FOUND OR PQ_FOUND)
file(GLOB source_database
src/database/*.[hc]pp)
add_library(database OBJECT ${source_database})
- include_directories(database ${SQLITE3_INCLUDE_DIRS})
+ if(SQLITE3_FOUND)
+ include_directories(database ${SQLITE3_INCLUDE_DIRS})
+ endif()
+ if(PQ_FOUND)
+ include_directories(database ${PQ_INCLUDE_DIRS})
+ endif()
set(USE_DATABASE TRUE)
else()
add_library(database OBJECT "")
@@ -260,8 +284,14 @@ if(LIBIDN_FOUND)
target_link_libraries(test_suite ${LIBIDN_LIBRARIES})
endif()
if(USE_DATABASE)
- target_link_libraries(${PROJECT_NAME} ${SQLITE3_LIBRARIES})
- target_link_libraries(test_suite ${SQLITE3_LIBRARIES})
+ if(SQLITE3_FOUND)
+ target_link_libraries(${PROJECT_NAME} ${SQLITE3_LIBRARIES})
+ target_link_libraries(test_suite ${SQLITE3_LIBRARIES})
+ endif()
+ if(PQ_FOUND)
+ target_link_libraries(${PROJECT_NAME} ${PQ_LIBRARIES})
+ target_link_libraries(test_suite ${PQ_LIBRARIES})
+endif()
endif()
# Define a __FILENAME__ macro with the relative path (from the base project directory)
diff --git a/INSTALL.rst b/INSTALL.rst
index 3cf55a2..45a860d 100644
--- a/INSTALL.rst
+++ b/INSTALL.rst
@@ -16,7 +16,7 @@ Build and runtime dependencies:
Tools:
~~~~~~
-- A C++14 compiler (clang >= 3.4 or gcc >= 4.9 for example)
+- A C++14 compiler (clang >= 3.4 or gcc >= 5.0 for example)
- CMake
- pandoc (optional) to build the man page
@@ -32,11 +32,11 @@ libiconv_
libuuid_
Generate unique IDs
-sqlite3_ (option, but highly recommended)
- Provides a way to store various options in a (sqlite3) database. Each user
- of the gateway can store their own values (for example their prefered port,
- or their IRC password). Without this dependency, many interesting features
- are missing.
+sqlite3_ or libpq_ (optional, but recommented)
+ Provides a way to store various options and messages archives in a
+ database. Each user of the gateway can store their own values (for
+ example their prefered port, or their IRC password). Without this
+ dependency, many interesting features are missing.
libidn_ (optional, but recommended)
Provides the stringprep functionality. Without it, JIDs for IRC users are
@@ -47,12 +47,13 @@ udns_ (optional, but recommended)
performances when connecting to a big number of IRC servers at the same
time.
-libbotan_ 2.x (optional)
+libbotan_ 2.x (optional, but recommended)
Provides TLS support. Without it, IRC connections are all made in
plain-text mode.
gcrypt_ (mandatory only if botan is absent)
- Provides the SHA-1 hash function, for the case where Botan is absent.
+ Provides the SHA-1 hash function, for the case where Botan is absent. It
+ does NOT provide any TLS or encryption feature.
systemd_ (optional)
Provides the support for a systemd service of Type=notify. This is useful only
@@ -96,6 +97,9 @@ The list of available options:
- POLL: use the standard poll(2). This is the default value on all non-Linux
platforms.
+- DEBUG_SQL_QUERIES: If set to ON, additional debug logging and timing will be
+ done for every SQL query that is executed. The default is OFF.
+
- WITH_BOTAN and WITHOUT_BOTAN: The first force the usage of the Botan library,
if it is not found, the configuration process will fail. The second will
make the build process ignore the Botan library, it will not be used even
@@ -165,3 +169,4 @@ to use biboumi.
.. _systemd: https://www.freedesktop.org/wiki/Software/systemd/
.. _biboumi.1.rst: doc/biboumi.1.rst
.. _gcrypt: https://www.gnu.org/software/libgcrypt/
+.. _libpq: https://www.postgresql.org/docs/current/static/libpq.html
diff --git a/README.rst b/README.rst
index 5ee9846..8a03701 100644
--- a/README.rst
+++ b/README.rst
@@ -4,9 +4,6 @@ Biboumi
.. image:: https://lab.louiz.org/louiz/biboumi/badges/master/build.svg
:target: https://lab.louiz.org/louiz/biboumi/pipelines
-.. image:: https://codecov.proxy.louiz.org/gh/louiz/biboumi/branch/master/graph/badge.svg
- :target: https://codecov.io/gh/louiz/biboumi
-
.. image:: https://coverity.proxy.louiz.org/projects/3726/badge.svg
:target: https://scan.coverity.com/projects/louiz-biboumi
diff --git a/cmake/Modules/FindPQ.cmake b/cmake/Modules/FindPQ.cmake
new file mode 100644
index 0000000..e268b8f
--- /dev/null
+++ b/cmake/Modules/FindPQ.cmake
@@ -0,0 +1,43 @@
+# - Find libpq
+# Find the postgresql front end library
+#
+# This module defines the following variables:
+# PQ_FOUND - True if library and include directory are found
+# If set to TRUE, the following are also defined:
+# PQ_INCLUDE_DIRS - The directory where to find the header file
+# PQ_LIBRARIES - Where to find the library file
+#
+# For conveniance, these variables are also set. They have the same values
+# than the variables above. The user can thus choose his/her prefered way
+# to write them.
+# PQ_LIBRARY
+# PQ_INCLUDE_DIR
+#
+# This file is in the public domain
+
+include(FindPkgConfig)
+
+if(NOT PQ_FOUND)
+ pkg_check_modules(PQ libpq)
+endif()
+
+if(NOT PQ_FOUND)
+ find_path(PQ_INCLUDE_DIRS NAMES libpq-fe.h
+ DOC "The libpq include directory")
+
+ find_library(PQ_LIBRARIES NAMES pq
+ DOC "The pq library")
+
+ # Use some standard module to handle the QUIETLY and REQUIRED arguments, and
+ # set PQ_FOUND to TRUE if these two variables are set.
+ include(FindPackageHandleStandardArgs)
+ find_package_handle_standard_args(PQ REQUIRED_VARS PQ_LIBRARIES PQ_INCLUDE_DIRS)
+
+ if(PQ_FOUND)
+ set(PQ_LIBRARY ${PQ_LIBRARIES} CACHE INTERNAL "")
+ set(PQ_INCLUDE_DIR ${PQ_INCLUDE_DIRS} CACHE INTERNAL "")
+ set(PQ_FOUND ${PQ_FOUND} CACHE INTERNAL "")
+ endif()
+endif()
+
+mark_as_advanced(PQ_INCLUDE_DIRS PQ_LIBRARIES)
diff --git a/conf/irc.gimp.org.policy.txt b/conf/irc.gimp.org.policy.txt
new file mode 100644
index 0000000..2357a53
--- /dev/null
+++ b/conf/irc.gimp.org.policy.txt
@@ -0,0 +1 @@
+key_exchange_methods = RSA
diff --git a/conf/irc.gnome.org.policy.txt b/conf/irc.gnome.org.policy.txt
new file mode 100644
index 0000000..2357a53
--- /dev/null
+++ b/conf/irc.gnome.org.policy.txt
@@ -0,0 +1 @@
+key_exchange_methods = RSA
diff --git a/conf/irc.ppirc.net.policy.txt b/conf/irc.ppirc.net.policy.txt
index 557d129..2357a53 100644
--- a/conf/irc.ppirc.net.policy.txt
+++ b/conf/irc.ppirc.net.policy.txt
@@ -1,2 +1 @@
key_exchange_methods = RSA
-minimum_rsa_bits = 1024
diff --git a/doc/biboumi.1.rst b/doc/biboumi.1.rst
index 3de4160..89f8627 100644
--- a/doc/biboumi.1.rst
+++ b/doc/biboumi.1.rst
@@ -29,7 +29,7 @@ Available command line options:
config_filename
---------------
-Specify the file to read for configuration. See *CONFIG* section for more
+Specify the file to read for configuration. See the `Configuration`_ section for more
details on its content.
Configuration
@@ -77,6 +77,20 @@ port
The TCP port to use to connect to the local XMPP component. The default
value is 5347.
+db_name
+-------
+
+The name of the database to use. This option can only be used if biboumi
+has been compiled with a database support (Sqlite3 and/or PostgreSQL). If
+the value begins with the postgresql scheme, “postgresql://” or
+“postgres://”, then biboumi will try to connect to the PostgreSQL database
+specified by the URI. See
+https://www.postgresql.org/docs/current/static/libpq-connect.html#idm46428693970032
+for all possible values. For example the value could be
+“postgresql://user:secret@localhost”. If the value does not start with the
+postgresql scheme, then it specifies a filename that will be opened with
+Sqlite3. For example the value could be “/var/lib/biboumi/biboumi.sqlite”.
+
admin
-----
@@ -100,6 +114,21 @@ be used by an administrator that just wants to let their users join their own
IRC server using an XMPP client, while forbidding access to any other IRC
server.
+persistent_by_default
+---------------------
+
+If this option is set to `true`, all rooms will be persistent by default:
+the value of the “persistent” option in the global configuration of each
+user will be “true”, but the value of each individual room will still
+default to false. This means that a user just needs to change the global
+“persistent” configuration option to false in order to override this.
+
+If it is set to false (the default value), all rooms are not persistent by
+default.
+
+Each room can be configured individually by each user, to override this
+default value. See `Ad-hoc commands`_.
+
realname_customization
----------------------
@@ -268,7 +297,7 @@ ISUPPORT extension) then it’s a channel name, otherwise this is a nickname.
As a special case, the channel name can also be empty (for example
``%irc.example.com``), in that case this represents the virtual channel
-provided by biboumi. See *Connect to an IRC server* for more details.
+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
@@ -362,7 +391,7 @@ IRC server JID
--------------
These presence will appear online in the user’s roster whenever they are
-connected to that IRC server (see *Connect to an IRC server* for more
+connected to that IRC server (see `Connect to an IRC server`_ for more
details). This is useful to keep track of which server an user is connected
to: this is sometimes hard to remember, when they have many clients, or if
they are using persistent channels.
@@ -381,16 +410,25 @@ History
-------
Public channel messages are saved into archives, inside the database, unless
-the `record_history` option is set to false by that user (see `Ad-hoc commands`).
+the `record_history` option is set to false by that user (see `Ad-hoc commands`_).
Private messages (messages that are sent directly to a nickname, not a
-channel) are never stored in the database. When a channel is joined, biboumi
-sends the `max_history_length` messages found in the database as the MUC
-history.
+channel) are never stored in the database.
A channel history can be retrieved by using `Message archive management (MAM)
<https://xmpp.org/extensions/xep-0313.htm>`_ on the channel JID. The results
can be filtered by start and end dates.
+When a channel is joined, if the client doesn’t specify any limit, biboumi
+sends the `max_history_length` last messages found in the database as the
+MUC history. If a client wants to only use MAM for the archives (because
+it’s more convenient and powerful), it should request to receive no
+history by using an attribute maxchars='0' or maxstanzas='0' as defined in
+XEP 0045, and do a proper MAM request instead.
+
+Note: the maxchars attribute is ignored unless its value is exactly 0.
+Supporting it properly would be very hard and would introduce a lot of
+complexity for almost no benefit.
+
For a given channel, each user has her or his own archive. The content of
the archives are never shared, and thus a user can not use someone else’s
archive to get the messages that they didn’t receive when they were offline.
@@ -586,10 +624,13 @@ On the gateway itself (e.g on the JID biboumi.example.com):
the database.
* Max history length: The maximum number of lines in the history
that the server is allowed to send when joining a channel.
- * Persistent: Overrides the value specified in each individual channel,
- all channels are persistent, whether or not their specific value is
- true or false. See below for more details on what a persistent
- channel is.
+
+ * Persistent: Overrides the value specified in each individual channel.
+ If this option is set to true, all channels are persistent, whether
+ or not their specific value is true or false. This option is true by
+ default for everyone if the `persistent_by_default` configuration
+ option is true, otherwise it’s false. See below for more details on
+ what a persistent channel is. This value is
On a server JID (e.g on the JID chat.freenode.org@biboumi.example.com)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -675,7 +716,7 @@ Raw IRC messages
Biboumi tries to support as many IRC features as possible, but doesn’t
handle everything yet (or ever). In order to let the user send any
arbitrary IRC message, biboumi forwards any XMPP message received on an IRC
-Server JID (see *ADDRESSING*) as a raw command to that IRC server.
+Server JID (see `Addressing`_) as a raw command to that IRC server.
For example, to WHOIS the user Foo on the server irc.example.com, a user can
send the message “WHOIS Foo” to ``irc.example.com@biboumi.example.com``.
diff --git a/docker/biboumi-test/alpine/Dockerfile b/docker/biboumi-test/alpine/Dockerfile
index f97c58c..e43f1b6 100644
--- a/docker/biboumi-test/alpine/Dockerfile
+++ b/docker/biboumi-test/alpine/Dockerfile
@@ -32,7 +32,8 @@ RUN apk add --no-cache g++\
openssl\
libressl-dev\
zlib-dev\
- curl
+ curl\
+ postgresql-dev
# Install botan
RUN git clone https://github.com/randombit/botan.git && cd botan && ./configure.py --prefix=/usr && make -j8 && make install && rm -rf /botan
diff --git a/docker/biboumi-test/debian/Dockerfile b/docker/biboumi-test/debian/Dockerfile
index 3a1c1a7..557face 100644
--- a/docker/biboumi-test/debian/Dockerfile
+++ b/docker/biboumi-test/debian/Dockerfile
@@ -39,13 +39,14 @@ RUN apt install -y g++\
openssl\
zlib1g-dev\
libssl-dev\
- curl
+ curl\
+ libpq-dev
# Install botan
RUN git clone https://github.com/randombit/botan.git && cd botan && ./configure.py --prefix=/usr && make -j8 && make install && rm -rf /botan
# Install slixmpp, for e2e tests
-RUN git clone https://github.com/saghul/aiodns.git && cd aiodns && git checkout 7ee13f9bea25784322~ && python3 setup.py build && python3 setup.py install && git clone git://git.louiz.org/slixmpp && pip3 install pyasn1 && cd slixmpp && python3 setup.py build && python3 setup.py install
+RUN git clone https://github.com/saghul/aiodns.git && cd aiodns && git checkout 7ee13f9bea25784322~ && python3 setup.py build && python3 setup.py install && git clone git://git.louiz.org/slixmpp && pip3 install pyasn1==0.4.2 && cd slixmpp && python3 setup.py build && python3 setup.py install
RUN useradd tester -m
diff --git a/docker/biboumi-test/fedora/Dockerfile b/docker/biboumi-test/fedora/Dockerfile
index 8ff418c..12e13e5 100644
--- a/docker/biboumi-test/fedora/Dockerfile
+++ b/docker/biboumi-test/fedora/Dockerfile
@@ -39,6 +39,7 @@ RUN dnf --refresh install -y\
openssl-devel\
which\
java-1.8.0-openjdk\
+ postgresql-devel\
&& dnf clean all
# Install botan
diff --git a/docker/biboumi/alpine/Dockerfile b/docker/biboumi/alpine/Dockerfile
index c1bf5fd..0b59eb7 100644
--- a/docker/biboumi/alpine/Dockerfile
+++ b/docker/biboumi/alpine/Dockerfile
@@ -13,6 +13,7 @@ RUN apk add --no-cache\
make\
udns-dev\
sqlite-dev\
+ postgresql-dev\
libuuid\
util-linux-dev\
expat-dev\
@@ -30,6 +31,7 @@ RUN git clone git://git.louiz.org/biboumi && mkdir ./biboumi/build && cd ./bibou
-DWITH_BOTAN=1\
-DWITH_SQLITE3=1\
-DWITH_LIBIDN=1\
+ -DWITH_POSTGRESQL=1\
&& make -j8 && make install && rm -rf /biboumi
RUN adduser biboumi -D -h /home/biboumi
diff --git a/docker/biboumi/alpine/README.md b/docker/biboumi/alpine/README.md
index 4b9e1e5..6385e94 100644
--- a/docker/biboumi/alpine/README.md
+++ b/docker/biboumi/alpine/README.md
@@ -38,6 +38,7 @@ The configuration file inside the image contains only a few default values. To
* BIBOUMI_PASSWORD: Sets the value of the *password* option.
* BIBOUMI_ADMIN: Sets the value of the *admin* option.
* BIBOUMI_XMPP_SERVER_IP: Sets the value of the *xmpp_server_ip* option. The default value is **xmpp**.
+* BIBOUMI_DB_NAME: Sets the database name to be used by biboumi: a filesystem path pointing at a Sqlite3 file, or a postgresql URI (starting with “postgresql://”). See below to learn how to mount a host directory (to save your Sqlite3 database) or how to link with a postgresql docker container.
You can also directly provide your own configuration file by mounting it inside the container using the -v option:
@@ -59,10 +60,33 @@ If you want to connect to the XMPP server running on the host machine, use the *
Volumes
-------
-The database is stored in the /var/lib/biboumi/ directory. If you don’t bind a local directory to it, the database will be lost when the container is stopped. If you want to keep your database between each run, bind it with the -v option, like this: **-v /srv/biboumi/:/var/lib/biboumi**.
+By default, a sqlite3 database is stored in the /var/lib/biboumi/ directory. If you don’t bind a local directory to it, the database will be lost when the container is stopped. If you want to keep your database between each run, bind it with the -v option, like this: **-v /srv/biboumi/:/var/lib/biboumi**.
-Note: Due to a limitation in Docker, to be able to read and write into this database, make sure this mounted directory is owned by UID and GID 1001:1001, on the host.
+Note: Due to a limitation in Docker, to be able to read and write into this database, make sure this mounted directory has the proper read and write permissions on the host: it can be owned by UID and GID 1000:1000, or use chmod to give permissions to everyone, for example.
```
-chown -R 1001:1001 database/
+chown -R 1000:1000 database/
+chmod 777 database/
+```
+
+Linking with a PostgreSQL container
+-----------------------------------
+
+If you want to use a PostgreSQL database, you need to either access the host database (run the biboumi container with --network=host), or link with a [postgresql docker image](https://hub.docker.com/_/postgres/).
+
+To do that, start the PostgreSQL container like this:
+
+```
+docker run --name postgres postgres:latest
+```
+
+This will run a postgresql instance with a configured superuser named “postgres”, with no password and a database named “postgres” as well. If you want different values, please refer to the PostgreSQL’s image documentation.
+
+Then start your biboumi container, by linking with this PostgreSQL container, and by specifying the correct db_name value (of course, also specify all the other options, like the XMPP hostname and password):
+
+```
+docker run --name biboumi \
+ --link=postgres \
+ -e BIBOUMI_DB_NAME=postgres://postgres@postgres/postgres \
+ biboumi
```
diff --git a/docker/biboumi-test/archlinux/Dockerfile b/docker/packaging/archlinux/Dockerfile
index 20f0343..20f0343 100644
--- a/docker/biboumi-test/archlinux/Dockerfile
+++ b/docker/packaging/archlinux/Dockerfile
diff --git a/packaging/biboumi.spec.cmake b/packaging/biboumi.spec.cmake
index d28c840..d07ed74 100644
--- a/packaging/biboumi.spec.cmake
+++ b/packaging/biboumi.spec.cmake
@@ -12,6 +12,7 @@ BuildRequires: expat-devel
BuildRequires: libuuid-devel
BuildRequires: systemd-devel
BuildRequires: sqlite-devel
+BuildRequires: postgresql-devel
BuildRequires: cmake
BuildRequires: systemd
BuildRequires: pandoc
@@ -39,7 +40,8 @@ cmake . -DCMAKE_CXX_FLAGS="%{optflags}" \
-DWITHOUT_BOTAN=1 \
-DWITH_SYSTEMD=1 \
-DWITH_LIBIDN=1 \
- -DWITH_SQLITE3=1
+ -DWITH_SQLITE3=1 \
+ -DWITH_POSTGRESQL=1
make %{?_smp_mflags}
@@ -64,8 +66,20 @@ make check %{?_smp_mflags}
* ${RPM_DATE} Le Coz Florent <louiz@louiz.org> - ${RPM_VERSION}-1
- Build latest git revision
-* Wed Jun 14 2017 Le Coz Florent <louiz@louiz.org> - 6.0-1
- Enable database support by building with sqlite3
+* Wed Jan 24 2018 Le Coz Florent <louiz@louiz.org> - 7.2-1
+ Update to version 7.2
+
+* Wed Jan 22 2018 Le Coz Florent <louiz@louiz.org> - 7.1-1
+ Update to version 7.1
+
+* Wed Jan 17 2018 Le Coz Florent <louiz@louiz.org> - 7.0-1
+ Update to version 7.0
+
+* Wed Oct 4 2017 Le Coz Florent <louiz@louiz.org> - 6.1-1
+ Update to version 6.1
+
+* Tue Sep 19 2017 Le Coz Florent <louiz@louiz.org> - 6.0-1
+ Update to version 6.0
* Wed May 24 2017 Le Coz Florent <louiz@louiz.org> - 5.0-1
- Update to version 5.0
diff --git a/scripts/dump_sqlite3.sh b/scripts/dump_sqlite3.sh
new file mode 100755
index 0000000..88f3097
--- /dev/null
+++ b/scripts/dump_sqlite3.sh
@@ -0,0 +1,21 @@
+#!/bin/sh
+
+sqlite3_args=$@
+
+function dump_table {
+ table=$1
+ columns=$2
+ echo ".mode insert $table
+.output $table.sql
+select $columns from $table;" | sqlite3 $sqlite3_args
+}
+
+dump_table "roster" "local, remote"
+
+dump_table "ircserveroptions_" "id_, owner_, server_, pass_, afterconnectioncommand_, tlsports_, ports_, username_, realname_, verifycert_, trustedfingerprint_, encodingout_, encodingin_, maxhistorylength_"
+
+dump_table "ircchanneloptions_" "id_, owner_, server_, channel_, encodingout_, encodingin_, maxhistorylength_, persistent_, recordhistory_"
+
+dump_table "globaloptions_" "id_, owner_, maxhistorylength_, recordhistory_, persistent_"
+
+dump_table "muclogline_" "id_, uuid_, owner_, ircchanname_, ircservername_, date_, body_, nick_"
diff --git a/src/biboumi.h.cmake b/src/biboumi.h.cmake
index 1ad9a40..fa99cd4 100644
--- a/src/biboumi.h.cmake
+++ b/src/biboumi.h.cmake
@@ -6,7 +6,11 @@
#cmakedefine BOTAN_FOUND
#cmakedefine GCRYPT_FOUND
#cmakedefine UDNS_FOUND
+#cmakedefine PQ_FOUND
+#cmakedefine SQLITE3_FOUND
#cmakedefine SOFTWARE_VERSION "${SOFTWARE_VERSION}"
#cmakedefine PROJECT_NAME "${PROJECT_NAME}"
#cmakedefine HAS_GET_TIME
#cmakedefine HAS_PUT_TIME
+#cmakedefine DEBUG_SQL_QUERIES
+
diff --git a/src/bridge/bridge.cpp b/src/bridge/bridge.cpp
index e0cb36d..54bee84 100644
--- a/src/bridge/bridge.cpp
+++ b/src/bridge/bridge.cpp
@@ -22,15 +22,12 @@ static std::string in_encoding_for(const Bridge& bridge, const Iid& iid)
{
#ifdef USE_DATABASE
const auto jid = bridge.get_bare_jid();
- auto options = Database::get_irc_channel_options_with_server_default(jid, iid.get_server(), iid.get_local());
- auto result = options.col<Database::EncodingIn>();
- if (!result.empty())
- return result;
+ return Database::get_encoding_in(jid, iid.get_server(), iid.get_local());
#else
(void)bridge;
(void)iid;
-#endif
return {"ISO-8859-1"};
+#endif
}
Bridge::Bridge(std::string user_jid, BiboumiComponent& xmpp, std::shared_ptr<Poller>& poller):
@@ -170,10 +167,11 @@ IrcClient* Bridge::find_irc_client(const std::string& hostname) const
}
bool Bridge::join_irc_channel(const Iid& iid, const std::string& nickname, const std::string& password,
- const std::string& resource)
+ const std::string& resource, HistoryLimit history_limit)
{
const auto& hostname = iid.get_server();
IrcClient* irc = this->make_irc_client(hostname, nickname);
+ irc->history_limit = history_limit;
this->add_resource_to_server(hostname, resource);
auto res_in_chan = this->is_resource_in_chan(ChannelKey{iid.get_local(), hostname}, resource);
if (!res_in_chan)
@@ -437,7 +435,7 @@ void Bridge::leave_irc_channel(Iid&& iid, const std::string& status_message, con
bool persistent = false;
#ifdef USE_DATABASE
const auto goptions = Database::get_global_options(this->user_jid);
- if (goptions.col<Database::Persistent>())
+ if (goptions.col<Database::GlobalPersistent>())
persistent = true;
else
{
@@ -470,9 +468,9 @@ void Bridge::leave_irc_channel(Iid&& iid, const std::string& status_message, con
"Biboumi note: " + std::to_string(resources - 1) + " resources are still in this channel.",
true, true, resource);
this->remove_resource_from_chan(key, resource);
+ }
if (this->number_of_channels_the_resource_is_in(iid.get_server(), resource) == 0)
this->remove_resource_from_server(iid.get_server(), resource);
- }
}
@@ -864,7 +862,8 @@ void Bridge::send_message(const Iid& iid, const std::string& nick, const std::st
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, true);
+ this->user_jid + "/"
+ + resource, "chat", true, true, true);
}
else
{
@@ -896,7 +895,19 @@ void Bridge::send_muc_leave(const Iid& iid, const std::string& nick,
this->xmpp.send_muc_leave(std::to_string(iid), nick, this->make_xmpp_body(message),
this->user_jid + "/" + res, self, user_requested);
if (self)
- this->remove_all_resources_from_chan(iid.to_tuple());
+ {
+ // Copy the resources currently in that channel
+ const auto resources_in_chan = this->resources_in_chan[iid.to_tuple()];
+
+ this->remove_all_resources_from_chan(iid.to_tuple());
+
+ // Now, for each resource that was in that channel, remove it from the server if it’s
+ // not in any other channel
+ for (const auto& r: resources_in_chan)
+ if (this->number_of_channels_the_resource_is_in(iid.get_server(), r) == 0)
+ this->remove_resource_from_server(iid.get_server(), r);
+
+ }
}
IrcClient* irc = this->find_irc_client(iid.get_server());
@@ -935,7 +946,10 @@ void Bridge::send_xmpp_message(const std::string& from, const std::string& autho
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", false, false);
+ if (Config::get("fixed_irc_server", "").empty())
+ this->xmpp.send_message(from, this->make_xmpp_body(body, encoding), this->user_jid + "/" + resource, "chat", false, true);
+ else
+ this->xmpp.send_message("", this->make_xmpp_body(body, encoding), this->user_jid + "/" + resource, "chat", false, true);
}
}
@@ -950,7 +964,7 @@ void Bridge::send_user_join(const std::string& hostname, const std::string& chan
const Iid iid(chan_name, hostname, Iid::Type::Channel);
this->send_xmpp_invitation(iid, "");
}
- else
+ else
{
for (const auto& resource: resources)
this->send_user_join(hostname, chan_name, user, user_mode, self, resource);
@@ -993,17 +1007,20 @@ void Bridge::send_topic(const std::string& hostname, const std::string& chan_nam
}
-void Bridge::send_room_history(const std::string& hostname, const std::string& chan_name)
+void Bridge::send_room_history(const std::string& hostname, const std::string& chan_name, const HistoryLimit& history_limit)
{
for (const auto& resource: this->resources_in_chan[ChannelKey{chan_name, hostname}])
- this->send_room_history(hostname, chan_name, resource);
+ this->send_room_history(hostname, chan_name, resource, history_limit);
}
-void Bridge::send_room_history(const std::string& hostname, std::string chan_name, const std::string& resource)
+void Bridge::send_room_history(const std::string& hostname, std::string chan_name, const std::string& resource, const HistoryLimit& history_limit)
{
#ifdef USE_DATABASE
const auto coptions = Database::get_irc_channel_options_with_server_and_global_default(this->user_jid, hostname, chan_name);
- const auto lines = Database::get_muc_logs(this->user_jid, chan_name, hostname, coptions.col<Database::MaxHistoryLength>());
+ auto limit = coptions.col<Database::MaxHistoryLength>();
+ if (history_limit.stanzas >= 0 && history_limit.stanzas < limit)
+ limit = history_limit.stanzas;
+ const auto lines = Database::get_muc_logs(this->user_jid, chan_name, hostname, limit, history_limit.since);
chan_name.append(utils::empty_if_fixed_server("%" + hostname));
for (const auto& line: lines)
{
@@ -1015,6 +1032,7 @@ void Bridge::send_room_history(const std::string& hostname, std::string chan_nam
(void)hostname;
(void)chan_name;
(void)resource;
+ (void)history_limit;
#endif
}
@@ -1232,9 +1250,13 @@ std::size_t Bridge::number_of_channels_the_resource_is_in(const std::string& irc
std::size_t res = 0;
for (auto pair: this->resources_in_chan)
{
- if (std::get<0>(pair.first) == irc_hostname && pair.second.count(resource) != 0)
+ if (std::get<1>(pair.first) == irc_hostname && pair.second.count(resource) != 0)
res++;
}
+
+ IrcClient* irc = this->find_irc_client(irc_hostname);
+ if (irc && (irc->get_dummy_channel().joined || irc->get_dummy_channel().joining))
+ res++;
return res;
}
@@ -1257,7 +1279,7 @@ 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_room_history(iid.get_server(), iid.get_local(), resource, irc->history_limit);
this->send_topic(iid.get_server(), iid.get_encoded_local(), channel->topic, channel->topic_author, resource);
}
diff --git a/src/bridge/bridge.hpp b/src/bridge/bridge.hpp
index c10631b..c2f0233 100644
--- a/src/bridge/bridge.hpp
+++ b/src/bridge/bridge.hpp
@@ -2,6 +2,7 @@
#include <bridge/result_set_management.hpp>
#include <bridge/list_element.hpp>
+#include <bridge/history_limit.hpp>
#include <irc/irc_message.hpp>
#include <irc/irc_client.hpp>
@@ -74,7 +75,7 @@ public:
* Try to join an irc_channel, does nothing and return true if the channel
* was already joined.
*/
- bool join_irc_channel(const Iid& iid, const std::string& nickname, const std::string& password, const std::string& resource);
+ bool join_irc_channel(const Iid& iid, const std::string& nickname, const std::string& password, const std::string& resource, HistoryLimit history_limit);
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");
@@ -156,8 +157,8 @@ public:
/**
* 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, std::string chan_name, const std::string& resource);
+ void send_room_history(const std::string& hostname, const std::string& chan_name, const HistoryLimit& history_limit);
+ void send_room_history(const std::string& hostname, std::string chan_name, const std::string& resource, const HistoryLimit& history_limit);
/**
* Send a MUC message from some participant
*/
diff --git a/src/bridge/colors.hpp b/src/bridge/colors.hpp
index dceed74..25b085a 100644
--- a/src/bridge/colors.hpp
+++ b/src/bridge/colors.hpp
@@ -6,20 +6,12 @@
* vice versa.
*/
+#include <xmpp/body.hpp>
+
#include <string>
#include <memory>
#include <tuple>
-class XmlNode;
-
-namespace Xmpp
-{
-// Contains:
-// - an XMPP-valid UTF-8 body
-// - an XML node representing the XHTML-IM body, or null
- using body = std::tuple<const std::string, std::unique_ptr<XmlNode>>;
-}
-
#define IRC_FORMAT_BOLD_CHAR '\x02' // done
#define IRC_FORMAT_COLOR_CHAR '\x03' // done
#define IRC_FORMAT_RESET_CHAR '\x0F' // done
diff --git a/src/bridge/history_limit.hpp b/src/bridge/history_limit.hpp
new file mode 100644
index 0000000..9c75256
--- /dev/null
+++ b/src/bridge/history_limit.hpp
@@ -0,0 +1,8 @@
+#pragma once
+
+// Default values means no limit
+struct HistoryLimit
+{
+ int stanzas{-1};
+ std::string since{};
+};
diff --git a/src/config/config.cpp b/src/config/config.cpp
index 0f3d639..412b170 100644
--- a/src/config/config.cpp
+++ b/src/config/config.cpp
@@ -23,6 +23,14 @@ std::string Config::get(const std::string& option, const std::string& def)
return it->second;
}
+bool Config::get_bool(const std::string& option, const bool def)
+{
+ auto res = Config::get(option, "");
+ if (res.empty())
+ return def;
+ return res == "true";
+}
+
int Config::get_int(const std::string& option, const int& def)
{
std::string res = Config::get(option, "");
diff --git a/src/config/config.hpp b/src/config/config.hpp
index 2ba38cc..c5ef15d 100644
--- a/src/config/config.hpp
+++ b/src/config/config.hpp
@@ -44,6 +44,7 @@ public:
* the second argument as the default.
*/
static int get_int(const std::string&, const int&);
+ static bool get_bool(const std::string&, const bool);
/**
* Set a value for the given option. And write all the config
* in the file from which it was read if save is true.
diff --git a/src/database/column.hpp b/src/database/column.hpp
index 111f9ca..1f16bcf 100644
--- a/src/database/column.hpp
+++ b/src/database/column.hpp
@@ -13,5 +13,10 @@ struct Column
T value{};
};
-struct Id: Column<std::size_t> { static constexpr auto name = "id_";
- static constexpr auto options = "PRIMARY KEY AUTOINCREMENT"; };
+struct Id: Column<std::size_t> {
+ static constexpr std::size_t unset_value = static_cast<std::size_t>(-1);
+ static constexpr auto name = "id_";
+ static constexpr auto options = "PRIMARY KEY";
+
+ Id(): Column<std::size_t>(-1) {}
+};
diff --git a/src/database/count_query.hpp b/src/database/count_query.hpp
index 0dde63c..118ce44 100644
--- a/src/database/count_query.hpp
+++ b/src/database/count_query.hpp
@@ -2,11 +2,10 @@
#include <database/query.hpp>
#include <database/table.hpp>
+#include <database/statement.hpp>
#include <string>
-#include <sqlite3.h>
-
struct CountQuery: public Query
{
CountQuery(std::string name):
@@ -15,20 +14,20 @@ struct CountQuery: public Query
this->body += std::move(name);
}
- int64_t execute(sqlite3* db)
+ int64_t execute(DatabaseEngine& db)
{
- auto statement = this->prepare(db);
+#ifdef DEBUG_SQL_QUERIES
+ const auto timer = this->log_and_time();
+#endif
+ auto statement = db.prepare(this->body);
int64_t res = 0;
- if (sqlite3_step(statement.get()) == SQLITE_ROW)
- res = sqlite3_column_int64(statement.get(), 0);
+ if (statement->step() != StepResult::Error)
+ res = statement->get_column_int64(0);
else
{
log_error("Count request didn’t return a result");
return 0;
}
- if (sqlite3_step(statement.get()) != SQLITE_DONE)
- log_warning("Count request returned more than one result.");
-
return res;
}
};
diff --git a/src/database/database.cpp b/src/database/database.cpp
index 85c675e..3622963 100644
--- a/src/database/database.cpp
+++ b/src/database/database.cpp
@@ -6,40 +6,54 @@
#include <utils/get_first_non_empty.hpp>
#include <utils/time.hpp>
-#include <sqlite3.h>
+#include <config/config.hpp>
+#include <database/sqlite3_engine.hpp>
+#include <database/postgresql_engine.hpp>
-sqlite3* Database::db;
-Database::MucLogLineTable Database::muc_log_lines("MucLogLine_");
-Database::GlobalOptionsTable Database::global_options("GlobalOptions_");
-Database::IrcServerOptionsTable Database::irc_server_options("IrcServerOptions_");
-Database::IrcChannelOptionsTable Database::irc_channel_options("IrcChannelOptions_");
+#include <database/engine.hpp>
+#include <database/index.hpp>
+
+#include <memory>
+
+std::unique_ptr<DatabaseEngine> Database::db;
+Database::MucLogLineTable Database::muc_log_lines("muclogline_");
+Database::GlobalOptionsTable Database::global_options("globaloptions_");
+Database::IrcServerOptionsTable Database::irc_server_options("ircserveroptions_");
+Database::IrcChannelOptionsTable Database::irc_channel_options("ircchanneloptions_");
Database::RosterTable Database::roster("roster");
+std::map<Database::CacheKey, Database::EncodingIn::real_type> Database::encoding_in_cache{};
+Database::GlobalPersistent::GlobalPersistent():
+ Column<bool>{Config::get_bool("persistent_by_default", false)}
+{}
void Database::open(const std::string& filename)
{
// Try to open the specified database.
// Close and replace the previous database pointer if it succeeded. If it did
// not, just leave things untouched
- sqlite3* new_db;
- auto res = sqlite3_open_v2(filename.data(), &new_db, SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE, nullptr);
- if (res != SQLITE_OK)
- {
- log_error("Failed to open database file ", filename, ": ", sqlite3_errmsg(Database::db));
- throw std::runtime_error("");
- }
- Database::close();
- Database::db = new_db;
- Database::muc_log_lines.create(Database::db);
- Database::muc_log_lines.upgrade(Database::db);
- Database::global_options.create(Database::db);
- Database::global_options.upgrade(Database::db);
- Database::irc_server_options.create(Database::db);
- Database::irc_server_options.upgrade(Database::db);
- Database::irc_channel_options.create(Database::db);
- Database::irc_channel_options.upgrade(Database::db);
- Database::roster.create(Database::db);
- Database::roster.upgrade(Database::db);
+ std::unique_ptr<DatabaseEngine> new_db;
+ static const auto psql_prefix = "postgresql://"s;
+ static const auto psql_prefix2 = "postgres://"s;
+ if ((filename.substr(0, psql_prefix.size()) == psql_prefix) ||
+ (filename.substr(0, psql_prefix2.size()) == psql_prefix2))
+ new_db = PostgresqlEngine::open(filename);
+ else
+ new_db = Sqlite3Engine::open(filename);
+ if (!new_db)
+ return;
+ Database::db = std::move(new_db);
+ Database::muc_log_lines.create(*Database::db);
+ Database::muc_log_lines.upgrade(*Database::db);
+ Database::global_options.create(*Database::db);
+ Database::global_options.upgrade(*Database::db);
+ Database::irc_server_options.create(*Database::db);
+ Database::irc_server_options.upgrade(*Database::db);
+ Database::irc_channel_options.create(*Database::db);
+ Database::irc_channel_options.upgrade(*Database::db);
+ Database::roster.create(*Database::db);
+ Database::roster.upgrade(*Database::db);
+ create_index<Database::Owner, Database::IrcChanName, Database::IrcServerName>(*Database::db, "archive_index", Database::muc_log_lines.get_name());
}
@@ -49,7 +63,7 @@ Database::GlobalOptions Database::get_global_options(const std::string& owner)
request.where() << Owner{} << "=" << owner;
Database::GlobalOptions options{Database::global_options.get_name()};
- auto result = request.execute(Database::db);
+ auto result = request.execute(*Database::db);
if (result.size() == 1)
options = result.front();
else
@@ -63,7 +77,7 @@ Database::IrcServerOptions Database::get_irc_server_options(const std::string& o
request.where() << Owner{} << "=" << owner << " and " << Server{} << "=" << server;
Database::IrcServerOptions options{Database::irc_server_options.get_name()};
- auto result = request.execute(Database::db);
+ auto result = request.execute(*Database::db);
if (result.size() == 1)
options = result.front();
else
@@ -81,7 +95,7 @@ Database::IrcChannelOptions Database::get_irc_channel_options(const std::string&
" and " << Server{} << "=" << server <<\
" and " << Channel{} << "=" << channel;
Database::IrcChannelOptions options{Database::irc_channel_options.get_name()};
- auto result = request.execute(Database::db);
+ auto result = request.execute(*Database::db);
if (result.size() == 1)
options = result.front();
else
@@ -176,7 +190,7 @@ std::vector<Database::MucLogLine> Database::get_muc_logs(const std::string& owne
if (limit >= 0)
request.limit() << limit;
- auto result = request.execute(Database::db);
+ auto result = request.execute(*Database::db);
return {result.crbegin(), result.crend()};
}
@@ -197,7 +211,7 @@ void Database::delete_roster_item(const std::string& local, const std::string& r
query << " WHERE " << Database::RemoteJid{} << "=" << remote << \
" AND " << Database::LocalJid{} << "=" << local;
- query.execute(Database::db);
+// query.execute(*Database::db);
}
bool Database::has_roster_item(const std::string& local, const std::string& remote)
@@ -206,7 +220,7 @@ bool Database::has_roster_item(const std::string& local, const std::string& remo
query.where() << Database::LocalJid{} << "=" << local << \
" and " << Database::RemoteJid{} << "=" << remote;
- auto res = query.execute(Database::db);
+ auto res = query.execute(*Database::db);
return !res.empty();
}
@@ -216,19 +230,18 @@ std::vector<Database::RosterItem> Database::get_contact_list(const std::string&
auto query = Database::roster.select();
query.where() << Database::LocalJid{} << "=" << local;
- return query.execute(Database::db);
+ return query.execute(*Database::db);
}
std::vector<Database::RosterItem> Database::get_full_roster()
{
auto query = Database::roster.select();
- return query.execute(Database::db);
+ return query.execute(*Database::db);
}
void Database::close()
{
- sqlite3_close_v2(Database::db);
Database::db = nullptr;
}
diff --git a/src/database/database.hpp b/src/database/database.hpp
index c00c938..ec44543 100644
--- a/src/database/database.hpp
+++ b/src/database/database.hpp
@@ -7,12 +7,15 @@
#include <database/column.hpp>
#include <database/count_query.hpp>
+#include <database/engine.hpp>
+
#include <utils/optional_bool.hpp>
#include <chrono>
#include <string>
#include <memory>
+#include <map>
class Database
@@ -24,11 +27,11 @@ class Database
struct Owner: Column<std::string> { static constexpr auto name = "owner_"; };
- struct IrcChanName: Column<std::string> { static constexpr auto name = "ircChanName_"; };
+ struct IrcChanName: Column<std::string> { static constexpr auto name = "ircchanname_"; };
struct Channel: Column<std::string> { static constexpr auto name = "channel_"; };
- struct IrcServerName: Column<std::string> { static constexpr auto name = "ircServerName_"; };
+ struct IrcServerName: Column<std::string> { static constexpr auto name = "ircservername_"; };
struct Server: Column<std::string> { static constexpr auto name = "server_"; };
@@ -43,35 +46,38 @@ class Database
struct Ports: Column<std::string> { static constexpr auto name = "ports_";
Ports(): Column<std::string>("6667") {} };
- struct TlsPorts: Column<std::string> { static constexpr auto name = "tlsPorts_";
+ struct TlsPorts: Column<std::string> { static constexpr auto name = "tlsports_";
TlsPorts(): Column<std::string>("6697;6670") {} };
struct Username: Column<std::string> { static constexpr auto name = "username_"; };
struct Realname: Column<std::string> { static constexpr auto name = "realname_"; };
- struct AfterConnectionCommand: Column<std::string> { static constexpr auto name = "afterConnectionCommand_"; };
+ struct AfterConnectionCommand: Column<std::string> { static constexpr auto name = "afterconnectioncommand_"; };
- struct TrustedFingerprint: Column<std::string> { static constexpr auto name = "trustedFingerprint_"; };
+ struct TrustedFingerprint: Column<std::string> { static constexpr auto name = "trustedfingerprint_"; };
- struct EncodingOut: Column<std::string> { static constexpr auto name = "encodingOut_"; };
+ struct EncodingOut: Column<std::string> { static constexpr auto name = "encodingout_"; };
- struct EncodingIn: Column<std::string> { static constexpr auto name = "encodingIn_"; };
+ struct EncodingIn: Column<std::string> { static constexpr auto name = "encodingin_"; };
- struct MaxHistoryLength: Column<int> { static constexpr auto name = "maxHistoryLength_";
+ struct MaxHistoryLength: Column<int> { static constexpr auto name = "maxhistorylength_";
MaxHistoryLength(): Column<int>(20) {} };
- struct RecordHistory: Column<bool> { static constexpr auto name = "recordHistory_";
+ struct RecordHistory: Column<bool> { static constexpr auto name = "recordhistory_";
RecordHistory(): Column<bool>(true) {}};
- struct RecordHistoryOptional: Column<OptionalBool> { static constexpr auto name = "recordHistory_"; };
+ struct RecordHistoryOptional: Column<OptionalBool> { static constexpr auto name = "recordhistory_"; };
- struct VerifyCert: Column<bool> { static constexpr auto name = "verifyCert_";
+ struct VerifyCert: Column<bool> { static constexpr auto name = "verifycert_";
VerifyCert(): Column<bool>(true) {} };
struct Persistent: Column<bool> { static constexpr auto name = "persistent_";
Persistent(): Column<bool>(false) {} };
+ struct GlobalPersistent: Column<bool> { static constexpr auto name = "persistent_";
+ GlobalPersistent(); };
+
struct LocalJid: Column<std::string> { static constexpr auto name = "local"; };
struct RemoteJid: Column<std::string> { static constexpr auto name = "remote"; };
@@ -80,7 +86,7 @@ class Database
using MucLogLineTable = Table<Id, Uuid, Owner, IrcChanName, IrcServerName, Date, Body, Nick>;
using MucLogLine = MucLogLineTable::RowType;
- using GlobalOptionsTable = Table<Id, Owner, MaxHistoryLength, RecordHistory, Persistent>;
+ using GlobalOptionsTable = Table<Id, Owner, MaxHistoryLength, RecordHistory, GlobalPersistent>;
using GlobalOptions = GlobalOptionsTable::RowType;
using IrcServerOptionsTable = Table<Id, Owner, Server, Pass, AfterConnectionCommand, TlsPorts, Ports, Username, Realname, VerifyCert, TrustedFingerprint, EncodingOut, EncodingIn, MaxHistoryLength>;
@@ -130,7 +136,7 @@ class Database
static int64_t count(const TableType& table)
{
CountQuery query{table.get_name()};
- return query.execute(Database::db);
+ return query.execute(*Database::db);
}
static MucLogLineTable muc_log_lines;
@@ -138,9 +144,48 @@ class Database
static IrcServerOptionsTable irc_server_options;
static IrcChannelOptionsTable irc_channel_options;
static RosterTable roster;
- static sqlite3* db;
+ static std::unique_ptr<DatabaseEngine> db;
+
+ /**
+ * Some caches, to avoid doing very frequent query requests for a few options.
+ */
+ using CacheKey = std::tuple<std::string, std::string, std::string>;
+
+ static EncodingIn::real_type get_encoding_in(const std::string& owner,
+ const std::string& server,
+ const std::string& channel)
+ {
+ CacheKey channel_key{owner, server, channel};
+ auto it = Database::encoding_in_cache.find(channel_key);
+ if (it == Database::encoding_in_cache.end())
+ {
+ auto options = Database::get_irc_channel_options_with_server_default(owner, server, channel);
+ EncodingIn::real_type result = options.col<Database::EncodingIn>();
+ if (result.empty())
+ result = "ISO-8859-1";
+ it = Database::encoding_in_cache.insert(std::make_pair(channel_key, result)).first;
+ }
+ return it->second;
+ }
+ static void invalidate_encoding_in_cache(const std::string& owner,
+ const std::string& server,
+ const std::string& channel)
+ {
+ CacheKey channel_key{owner, server, channel};
+ Database::encoding_in_cache.erase(channel_key);
+ }
+ static void invalidate_encoding_in_cache()
+ {
+ Database::encoding_in_cache.clear();
+ }
+
+ static auto raw_exec(const std::string& query)
+ {
+ Database::db->raw_exec(query);
+ }
private:
static std::string gen_uuid();
+ static std::map<CacheKey, EncodingIn::real_type> encoding_in_cache;
};
#endif /* USE_DATABASE */
diff --git a/src/database/engine.hpp b/src/database/engine.hpp
new file mode 100644
index 0000000..41dccf5
--- /dev/null
+++ b/src/database/engine.hpp
@@ -0,0 +1,41 @@
+#pragma once
+
+/**
+ * Interface to provide non-portable behaviour, specific to each
+ * database engine we want to support.
+ *
+ * Everything else (all portable stuf) should go outside of this class.
+ */
+
+#include <database/statement.hpp>
+
+#include <memory>
+#include <string>
+#include <vector>
+#include <tuple>
+#include <set>
+
+class DatabaseEngine
+{
+ public:
+
+ DatabaseEngine() = default;
+ virtual ~DatabaseEngine() = default;
+
+ DatabaseEngine(const DatabaseEngine&) = delete;
+ DatabaseEngine& operator=(const DatabaseEngine&) = delete;
+ DatabaseEngine(DatabaseEngine&&) = delete;
+ DatabaseEngine& operator=(DatabaseEngine&&) = delete;
+
+ virtual std::set<std::string> get_all_columns_from_table(const std::string& table_name) = 0;
+ virtual std::tuple<bool, std::string> raw_exec(const std::string& query) = 0;
+ virtual std::unique_ptr<Statement> prepare(const std::string& query) = 0;
+ virtual void extract_last_insert_rowid(Statement& statement) = 0;
+ virtual std::string get_returning_id_sql_string(const std::string&)
+ {
+ return {};
+ }
+ virtual std::string id_column_type() = 0;
+
+ int64_t last_inserted_rowid{-1};
+};
diff --git a/src/database/index.hpp b/src/database/index.hpp
new file mode 100644
index 0000000..30766ab
--- /dev/null
+++ b/src/database/index.hpp
@@ -0,0 +1,38 @@
+#pragma once
+
+#include <database/engine.hpp>
+
+#include <string>
+#include <tuple>
+
+namespace
+{
+template <std::size_t N=0, typename... T>
+typename std::enable_if<N == sizeof...(T), void>::type
+add_column_name(std::string&)
+{ }
+
+template <std::size_t N=0, typename... T>
+typename std::enable_if<N < sizeof...(T), void>::type
+add_column_name(std::string& out)
+{
+ using ColumnType = typename std::remove_reference<decltype(std::get<N>(std::declval<std::tuple<T...>>()))>::type;
+ out += ColumnType::name;
+ if (N != sizeof...(T) - 1)
+ out += ",";
+ add_column_name<N+1, T...>(out);
+}
+}
+
+template <typename... Columns>
+void create_index(DatabaseEngine& db, const std::string& name, const std::string& table)
+{
+ std::string query{"CREATE INDEX IF NOT EXISTS "};
+ query += name + " ON " + table + "(";
+ add_column_name<0, Columns...>(query);
+ query += ")";
+
+ auto result = db.raw_exec(query);
+ if (std::get<0>(result) == false)
+ log_error("Error executing query: ", std::get<1>(result));
+}
diff --git a/src/database/insert_query.hpp b/src/database/insert_query.hpp
index 2ece69d..9726424 100644
--- a/src/database/insert_query.hpp
+++ b/src/database/insert_query.hpp
@@ -10,64 +10,64 @@
#include <string>
#include <tuple>
-#include <sqlite3.h>
-
-template <int N, typename ColumnType, typename... T>
-typename std::enable_if<!std::is_same<std::decay_t<ColumnType>, Id>::value, void>::type
-actual_bind(Statement& statement, std::vector<std::string>& params, const std::tuple<T...>&)
+template <std::size_t N=0, typename... T>
+typename std::enable_if<N < sizeof...(T), void>::type
+update_autoincrement_id(std::tuple<T...>& columns, Statement& statement)
{
- const auto value = params.front();
- params.erase(params.begin());
- if (sqlite3_bind_text(statement.get(), N + 1, value.data(), static_cast<int>(value.size()), SQLITE_TRANSIENT) != SQLITE_OK)
- log_error("Failed to bind ", value, " to param ", N);
+ using ColumnType = typename std::decay<decltype(std::get<N>(columns))>::type;
+ if (std::is_same<ColumnType, Id>::value)
+ auto&& column = std::get<Id>(columns);
+ update_autoincrement_id<N+1>(columns, statement);
}
-template <int N, typename ColumnType, typename... T>
-typename std::enable_if<std::is_same<std::decay_t<ColumnType>, Id>::value, void>::type
-actual_bind(Statement& statement, std::vector<std::string>&, const std::tuple<T...>& columns)
-{
- auto&& column = std::get<Id>(columns);
- if (column.value != 0)
- {
- if (sqlite3_bind_int64(statement.get(), N + 1, static_cast<sqlite3_int64>(column.value)) != SQLITE_OK)
- log_error("Failed to bind ", column.value, " to id.");
- }
- else if (sqlite3_bind_null(statement.get(), N + 1) != SQLITE_OK)
- log_error("Failed to bind NULL to param ", N);
-}
+template <std::size_t N=0, typename... T>
+typename std::enable_if<N == sizeof...(T), void>::type
+update_autoincrement_id(std::tuple<T...>&, Statement& statement)
+{}
struct InsertQuery: public Query
{
- InsertQuery(const std::string& name):
- Query("INSERT OR REPLACE INTO ")
+ template <typename... T>
+ InsertQuery(const std::string& name, const std::tuple<T...>& columns):
+ Query("INSERT INTO ")
{
this->body += name;
+ this->insert_col_names(columns);
+ this->insert_values(columns);
}
template <typename... T>
- void execute(const std::tuple<T...>& columns, sqlite3* db)
+ void execute(DatabaseEngine& db, std::tuple<T...>& columns)
{
- auto statement = this->prepare(db);
- {
- this->bind_param(columns, statement);
- if (sqlite3_step(statement.get()) != SQLITE_DONE)
- log_error("Failed to execute query: ", sqlite3_errmsg(db));
- }
+#ifdef DEBUG_SQL_QUERIES
+ const auto timer = this->log_and_time();
+#endif
+
+ auto statement = db.prepare(this->body);
+ this->bind_param(columns, *statement);
+
+ if (statement->step() != StepResult::Error)
+ db.extract_last_insert_rowid(*statement);
+ else
+ log_error("Failed to extract the rowid from the last INSERT");
}
template <int N=0, typename... T>
typename std::enable_if<N < sizeof...(T), void>::type
- bind_param(const std::tuple<T...>& columns, Statement& statement)
+ bind_param(const std::tuple<T...>& columns, Statement& statement, int index=1)
{
- using ColumnType = typename std::remove_reference<decltype(std::get<N>(columns))>::type;
+ auto&& column = std::get<N>(columns);
+ using ColumnType = std::decay_t<decltype(column)>;
- actual_bind<N, ColumnType>(statement, this->params, columns);
- this->bind_param<N+1>(columns, statement);
+ if (!std::is_same<ColumnType, Id>::value)
+ actual_bind(statement, column.value, index++);
+
+ this->bind_param<N+1>(columns, statement, index);
}
template <int N=0, typename... T>
typename std::enable_if<N == sizeof...(T), void>::type
- bind_param(const std::tuple<T...>&, Statement&)
+ bind_param(const std::tuple<T...>&, Statement&, int)
{}
template <typename... T>
@@ -80,18 +80,21 @@ struct InsertQuery: public Query
template <int N=0, typename... T>
typename std::enable_if<N < sizeof...(T), void>::type
- insert_value(const std::tuple<T...>& columns)
+ insert_value(const std::tuple<T...>& columns, int index=1)
{
- this->body += "?";
- if (N != sizeof...(T) - 1)
- this->body += ",";
- this->body += " ";
- add_param(*this, std::get<N>(columns));
- this->insert_value<N+1>(columns);
+ using ColumnType = std::decay_t<decltype(std::get<N>(columns))>;
+
+ if (!std::is_same<ColumnType, Id>::value)
+ {
+ this->body += "$" + std::to_string(index++);
+ if (N != sizeof...(T) - 1)
+ this->body += ", ";
+ }
+ this->insert_value<N+1>(columns, index);
}
template <int N=0, typename... T>
typename std::enable_if<N == sizeof...(T), void>::type
- insert_value(const std::tuple<T...>&)
+ insert_value(const std::tuple<T...>&, const int)
{ }
template <typename... T>
@@ -99,27 +102,28 @@ struct InsertQuery: public Query
{
this->body += " (";
this->insert_col_name(columns);
- this->body += ")\n";
+ this->body += ")";
}
template <int N=0, typename... T>
typename std::enable_if<N < sizeof...(T), void>::type
insert_col_name(const std::tuple<T...>& columns)
{
- using ColumnType = typename std::remove_reference<decltype(std::get<N>(columns))>::type;
+ using ColumnType = std::decay_t<decltype(std::get<N>(columns))>;
- this->body += ColumnType::name;
+ if (!std::is_same<ColumnType, Id>::value)
+ {
+ this->body += ColumnType::name;
- if (N < (sizeof...(T) - 1))
- this->body += ", ";
+ if (N < (sizeof...(T) - 1))
+ this->body += ", ";
+ }
this->insert_col_name<N+1>(columns);
}
+
template <int N=0, typename... T>
typename std::enable_if<N == sizeof...(T), void>::type
insert_col_name(const std::tuple<T...>&)
{}
-
-
- private:
};
diff --git a/src/database/postgresql_engine.cpp b/src/database/postgresql_engine.cpp
new file mode 100644
index 0000000..984a959
--- /dev/null
+++ b/src/database/postgresql_engine.cpp
@@ -0,0 +1,91 @@
+#include <biboumi.h>
+#ifdef PQ_FOUND
+
+#include <utils/scopeguard.hpp>
+
+#include <database/query.hpp>
+
+#include <database/postgresql_engine.hpp>
+
+#include <database/postgresql_statement.hpp>
+
+#include <logger/logger.hpp>
+
+PostgresqlEngine::PostgresqlEngine(PGconn*const conn):
+ conn(conn)
+{}
+
+PostgresqlEngine::~PostgresqlEngine()
+{
+ PQfinish(this->conn);
+}
+
+std::unique_ptr<DatabaseEngine> PostgresqlEngine::open(const std::string& conninfo)
+{
+ PGconn* con = PQconnectdb(conninfo.data());
+
+ if (!con)
+ {
+ log_error("Failed to allocate a Postgresql connection");
+ throw std::runtime_error("");
+ }
+ const auto status = PQstatus(con);
+ if (status != CONNECTION_OK)
+ {
+ const char* errmsg = PQerrorMessage(con);
+ log_error("Postgresql connection failed: ", errmsg);
+ throw std::runtime_error("failed to open connection.");
+ }
+ return std::make_unique<PostgresqlEngine>(con);
+}
+
+std::set<std::string> PostgresqlEngine::get_all_columns_from_table(const std::string& table_name)
+{
+ const auto query = "SELECT column_name from information_schema.columns where table_name='" + table_name + "'";
+ auto statement = this->prepare(query);
+ std::set<std::string> columns;
+
+ while (statement->step() == StepResult::Row)
+ columns.insert(statement->get_column_text(0));
+
+ return columns;
+}
+
+std::tuple<bool, std::string> PostgresqlEngine::raw_exec(const std::string& query)
+{
+#ifdef DEBUG_SQL_QUERIES
+ log_debug("SQL QUERY: ", query);
+ const auto timer = make_sql_timer();
+#endif
+ PGresult* res = PQexec(this->conn, query.data());
+ auto sg = utils::make_scope_guard([res](){
+ PQclear(res);
+ });
+
+ auto res_status = PQresultStatus(res);
+ if (res_status != PGRES_COMMAND_OK)
+ return std::make_tuple(false, PQresultErrorMessage(res));
+ return std::make_tuple(true, std::string{});
+}
+
+std::unique_ptr<Statement> PostgresqlEngine::prepare(const std::string& query)
+{
+ return std::make_unique<PostgresqlStatement>(query, this->conn);
+}
+
+void PostgresqlEngine::extract_last_insert_rowid(Statement& statement)
+{
+ this->last_inserted_rowid = statement.get_column_int64(0);
+}
+
+std::string PostgresqlEngine::get_returning_id_sql_string(const std::string& col_name)
+{
+ return " RETURNING " + col_name;
+}
+
+std::string PostgresqlEngine::id_column_type()
+{
+ return "SERIAL";
+}
+
+#endif
diff --git a/src/database/postgresql_engine.hpp b/src/database/postgresql_engine.hpp
new file mode 100644
index 0000000..fe4fb53
--- /dev/null
+++ b/src/database/postgresql_engine.hpp
@@ -0,0 +1,48 @@
+#pragma once
+
+#include <biboumi.h>
+#include <string>
+#include <stdexcept>
+#include <memory>
+
+#include <database/statement.hpp>
+#include <database/engine.hpp>
+
+#include <tuple>
+#include <set>
+
+#ifdef PQ_FOUND
+
+#include <libpq-fe.h>
+
+class PostgresqlEngine: public DatabaseEngine
+{
+ public:
+ PostgresqlEngine(PGconn*const conn);
+
+ ~PostgresqlEngine();
+
+ static std::unique_ptr<DatabaseEngine> open(const std::string& string);
+
+ std::set<std::string> get_all_columns_from_table(const std::string& table_name) override final;
+ std::tuple<bool, std::string> raw_exec(const std::string& query) override final;
+ std::unique_ptr<Statement> prepare(const std::string& query) override;
+ void extract_last_insert_rowid(Statement& statement) override;
+ std::string get_returning_id_sql_string(const std::string& col_name) override;
+ std::string id_column_type() override;
+private:
+ PGconn* const conn;
+};
+
+#else
+
+class PostgresqlEngine
+{
+public:
+ static std::unique_ptr<DatabaseEngine> open(const std::string& string)
+ {
+ throw std::runtime_error("Cannot open postgresql database "s + string + ": biboumi is not compiled with libpq.");
+ }
+};
+
+#endif
diff --git a/src/database/postgresql_statement.hpp b/src/database/postgresql_statement.hpp
new file mode 100644
index 0000000..571c8f1
--- /dev/null
+++ b/src/database/postgresql_statement.hpp
@@ -0,0 +1,123 @@
+#pragma once
+
+#include <database/statement.hpp>
+
+#include <logger/logger.hpp>
+
+#include <libpq-fe.h>
+
+class PostgresqlStatement: public Statement
+{
+ public:
+ PostgresqlStatement(std::string body, PGconn*const conn):
+ body(std::move(body)),
+ conn(conn)
+ {}
+ ~PostgresqlStatement()
+ {
+ PQclear(this->result);
+ this->result = nullptr;
+ }
+ PostgresqlStatement(const PostgresqlStatement&) = delete;
+ PostgresqlStatement& operator=(const PostgresqlStatement&) = delete;
+ PostgresqlStatement(PostgresqlStatement&& other) = delete;
+ PostgresqlStatement& operator=(PostgresqlStatement&& other) = delete;
+
+ StepResult step() override final
+ {
+ if (!this->executed)
+ {
+ this->current_tuple = 0;
+ this->executed = true;
+ if (!this->execute())
+ return StepResult::Error;
+ }
+ else
+ {
+ this->current_tuple++;
+ }
+ if (this->current_tuple < PQntuples(this->result))
+ return StepResult::Row;
+ return StepResult::Done;
+ }
+
+ int64_t get_column_int64(const int col) override
+ {
+ const char* result = PQgetvalue(this->result, this->current_tuple, col);
+ std::istringstream iss;
+ iss.str(result);
+ int64_t res;
+ iss >> res;
+ return res;
+ }
+ std::string get_column_text(const int col) override
+ {
+ const char* result = PQgetvalue(this->result, this->current_tuple, col);
+ return result;
+ }
+ int get_column_int(const int col) override
+ {
+ const char* result = PQgetvalue(this->result, this->current_tuple, col);
+ std::istringstream iss;
+ iss.str(result);
+ int res;
+ iss >> res;
+ return res;
+ }
+
+ void bind(std::vector<std::string> params) override
+ {
+
+ this->params = std::move(params);
+ }
+
+ bool bind_text(const int, const std::string& data) override
+ {
+ this->params.push_back(data);
+ return true;
+ }
+ bool bind_int64(const int, const std::int64_t value) override
+ {
+ this->params.push_back(std::to_string(value));
+ return true;
+ }
+ bool bind_null(const int) override
+ {
+ this->params.push_back("NULL");
+ return true;
+ }
+
+ private:
+
+private:
+ bool execute()
+ {
+ std::vector<const char*> params;
+ params.reserve(this->params.size());
+
+ for (const auto& param: this->params)
+ params.push_back(param.data());
+ const int param_size = static_cast<int>(this->params.size());
+ this->result = PQexecParams(this->conn, this->body.data(),
+ param_size,
+ nullptr,
+ params.data(),
+ nullptr,
+ nullptr,
+ 0);
+ const auto status = PQresultStatus(this->result);
+ if (status != PGRES_TUPLES_OK && status != PGRES_COMMAND_OK)
+ {
+ log_error("Failed to execute command: ", PQresultErrorMessage(this->result));
+ return false;
+ }
+ return true;
+ }
+
+ bool executed{false};
+ std::string body;
+ PGconn*const conn;
+ std::vector<std::string> params;
+ PGresult* result{nullptr};
+ int current_tuple{0};
+};
diff --git a/src/database/query.cpp b/src/database/query.cpp
index ba63a92..d27dc59 100644
--- a/src/database/query.cpp
+++ b/src/database/query.cpp
@@ -1,9 +1,26 @@
#include <database/query.hpp>
#include <database/column.hpp>
-template <>
-void add_param<Id>(Query&, const Id&)
-{}
+void actual_bind(Statement& statement, const std::string& value, int index)
+{
+ statement.bind_text(index, value);
+}
+
+void actual_bind(Statement& statement, const std::int64_t value, int index)
+{
+ statement.bind_int64(index, value);
+}
+
+void actual_bind(Statement& statement, const OptionalBool& value, int index)
+{
+ if (!value.is_set)
+ statement.bind_int64(index, 0);
+ else if (value.value)
+ statement.bind_int64(index, 1);
+ else
+ statement.bind_int64(index, -1);
+}
+
void actual_add_param(Query& query, const std::string& val)
{
@@ -28,7 +45,8 @@ Query& operator<<(Query& query, const char* str)
Query& operator<<(Query& query, const std::string& str)
{
- query.body += "?";
+ query.body += "$" + std::to_string(query.current_param);
+ query.current_param++;
actual_add_param(query, str);
return query;
}
diff --git a/src/database/query.hpp b/src/database/query.hpp
index 6e1db12..8434944 100644
--- a/src/database/query.hpp
+++ b/src/database/query.hpp
@@ -1,5 +1,7 @@
#pragma once
+#include <biboumi.h>
+
#include <utils/optional_bool.hpp>
#include <database/statement.hpp>
#include <database/column.hpp>
@@ -9,54 +11,53 @@
#include <vector>
#include <string>
-#include <sqlite3.h>
+void actual_bind(Statement& statement, const std::string& value, int index);
+void actual_bind(Statement& statement, const std::int64_t value, int index);
+void actual_bind(Statement& statement, const OptionalBool& value, int index);
+
+#ifdef DEBUG_SQL_QUERIES
+#include <utils/scopetimer.hpp>
+
+inline auto make_sql_timer()
+{
+ return make_scope_timer([](const std::chrono::steady_clock::duration& elapsed)
+ {
+ const auto seconds = std::chrono::duration_cast<std::chrono::seconds>(elapsed);
+ const auto rest = elapsed - seconds;
+ log_debug("Query executed in ", seconds.count(), ".", rest.count(), "s.");
+ });
+}
+#endif
struct Query
{
std::string body;
std::vector<std::string> params;
+ int current_param{1};
Query(std::string str):
body(std::move(str))
{}
- Statement prepare(sqlite3* db)
+#ifdef DEBUG_SQL_QUERIES
+ auto log_and_time()
{
- sqlite3_stmt* stmt;
- auto res = sqlite3_prepare(db, this->body.data(), static_cast<int>(this->body.size()) + 1,
- &stmt, nullptr);
- if (res != SQLITE_OK)
- {
- log_error("Error preparing statement: ", sqlite3_errmsg(db));
- return nullptr;
- }
- Statement statement(stmt);
- int i = 1;
- for (const std::string& param: this->params)
- {
- if (sqlite3_bind_text(statement.get(), i, param.data(), static_cast<int>(param.size()), SQLITE_TRANSIENT) != SQLITE_OK)
- log_error("Failed to bind ", param, " to param ", i);
- i++;
- }
-
- return statement;
- }
-
- void execute(sqlite3* db)
- {
- auto statement = this->prepare(db);
- while (sqlite3_step(statement.get()) != SQLITE_DONE)
- ;
+ std::ostringstream os;
+ os << this->body << "; ";
+ for (const auto& param: this->params)
+ os << "'" << param << "' ";
+ log_debug("SQL QUERY: ", os.str());
+ return make_sql_timer();
}
+#endif
};
template <typename ColumnType>
void add_param(Query& query, const ColumnType& column)
{
+ std::cout << "add_param<ColumnType>" << std::endl;
actual_add_param(query, column.value);
}
-template <>
-void add_param<Id>(Query& query, const Id& column);
template <typename T>
void actual_add_param(Query& query, const T& val)
@@ -81,7 +82,8 @@ template <typename Integer>
typename std::enable_if<std::is_integral<Integer>::value, Query&>::type
operator<<(Query& query, const Integer& i)
{
- query.body += "?";
+ query.body += "$" + std::to_string(query.current_param++);
actual_add_param(query, i);
return query;
}
+
diff --git a/src/database/row.hpp b/src/database/row.hpp
index 2b50874..4dc98be 100644
--- a/src/database/row.hpp
+++ b/src/database/row.hpp
@@ -1,72 +1,72 @@
#pragma once
#include <database/insert_query.hpp>
+#include <database/update_query.hpp>
#include <logger/logger.hpp>
-#include <type_traits>
-
-#include <sqlite3.h>
+#include <utils/is_one_of.hpp>
-template <typename ColumnType, typename... T>
-typename std::enable_if<!std::is_same<std::decay_t<ColumnType>, Id>::value, void>::type
-update_id(std::tuple<T...>&, sqlite3*)
-{}
+#include <type_traits>
-template <typename ColumnType, typename... T>
-typename std::enable_if<std::is_same<std::decay_t<ColumnType>, Id>::value, void>::type
-update_id(std::tuple<T...>& columns, sqlite3* db)
+template <typename... T>
+struct Row
{
- auto&& column = std::get<ColumnType>(columns);
- auto res = sqlite3_last_insert_rowid(db);
- column.value = static_cast<Id::real_type>(res);
-}
+ Row(std::string name):
+ table_name(std::move(name))
+ {}
-template <std::size_t N=0, typename... T>
-typename std::enable_if<N < sizeof...(T), void>::type
-update_autoincrement_id(std::tuple<T...>& columns, sqlite3* db)
-{
- using ColumnType = typename std::remove_reference<decltype(std::get<N>(columns))>::type;
- update_id<ColumnType>(columns, db);
- update_autoincrement_id<N+1>(columns, db);
-}
+ template <typename Type>
+ typename Type::real_type& col()
+ {
+ auto&& col = std::get<Type>(this->columns);
+ return col.value;
+ }
-template <std::size_t N=0, typename... T>
-typename std::enable_if<N == sizeof...(T), void>::type
-update_autoincrement_id(std::tuple<T...>&, sqlite3*)
-{}
+ template <typename Type>
+ const auto& col() const
+ {
+ auto&& col = std::get<Type>(this->columns);
+ return col.value;
+ }
-template <typename... T>
-struct Row
-{
- Row(std::string name):
- table_name(std::move(name))
- {}
+ template <bool Coucou=true>
+ void save(std::unique_ptr<DatabaseEngine>& db, typename std::enable_if<!is_one_of<Id, T...> && Coucou>::type* = nullptr)
+ {
+ this->insert(*db);
+ }
- template <typename Type>
- typename Type::real_type& col()
- {
- auto&& col = std::get<Type>(this->columns);
- return col.value;
- }
+ template <bool Coucou=true>
+ void save(std::unique_ptr<DatabaseEngine>& db, typename std::enable_if<is_one_of<Id, T...> && Coucou>::type* = nullptr)
+ {
+ const Id& id = std::get<Id>(this->columns);
+ if (id.value == Id::unset_value)
+ {
+ this->insert(*db);
+ if (db->last_inserted_rowid >= 0)
+ std::get<Id>(this->columns).value = static_cast<Id::real_type>(db->last_inserted_rowid);
+ }
+ else
+ this->update(*db);
+ }
- template <typename Type>
- const auto& col() const
- {
- auto&& col = std::get<Type>(this->columns);
- return col.value;
- }
+ private:
+ void insert(DatabaseEngine& db)
+ {
+ InsertQuery query(this->table_name, this->columns);
+ // Ugly workaround for non portable stuff
+ query.body += db.get_returning_id_sql_string(Id::name);
+ query.execute(db, this->columns);
+ }
- void save(sqlite3* db)
- {
- InsertQuery query(this->table_name);
- query.insert_col_names(this->columns);
- query.insert_values(this->columns);
+ void update(DatabaseEngine& db)
+ {
+ UpdateQuery query(this->table_name, this->columns);
- query.execute(this->columns, db);
+ query.execute(db, this->columns);
+ }
- update_autoincrement_id(this->columns, db);
- }
+public:
+ std::tuple<T...> columns;
+ std::string table_name;
- std::tuple<T...> columns;
- std::string table_name;
};
diff --git a/src/database/select_query.hpp b/src/database/select_query.hpp
index 872001c..5a17f38 100644
--- a/src/database/select_query.hpp
+++ b/src/database/select_query.hpp
@@ -1,5 +1,7 @@
#pragma once
+#include <database/engine.hpp>
+
#include <database/statement.hpp>
#include <database/query.hpp>
#include <logger/logger.hpp>
@@ -10,32 +12,27 @@
#include <vector>
#include <string>
-#include <sqlite3.h>
-
using namespace std::string_literals;
template <typename T>
-typename std::enable_if<std::is_integral<T>::value, sqlite3_int64>::type
+typename std::enable_if<std::is_integral<T>::value, std::int64_t>::type
extract_row_value(Statement& statement, const int i)
{
- return sqlite3_column_int64(statement.get(), i);
+ return statement.get_column_int64(i);
}
template <typename T>
typename std::enable_if<std::is_same<std::string, T>::value, T>::type
extract_row_value(Statement& statement, const int i)
{
- const auto size = sqlite3_column_bytes(statement.get(), i);
- const unsigned char* str = sqlite3_column_text(statement.get(), i);
- std::string result(reinterpret_cast<const char*>(str), static_cast<std::size_t>(size));
- return result;
+ return statement.get_column_text(i);
}
template <typename T>
typename std::enable_if<std::is_same<OptionalBool, T>::value, T>::type
extract_row_value(Statement& statement, const int i)
{
- const auto integer = sqlite3_column_int(statement.get(), i);
+ const auto integer = statement.get_column_int(i);
OptionalBool result;
if (integer > 0)
result.set_value(true);
@@ -109,16 +106,24 @@ struct SelectQuery: public Query
return *this;
}
- auto execute(sqlite3* db)
+ auto execute(DatabaseEngine& db)
{
- auto statement = this->prepare(db);
std::vector<Row<T...>> rows;
- while (sqlite3_step(statement.get()) == SQLITE_ROW)
+
+#ifdef DEBUG_SQL_QUERIES
+ const auto timer = this->log_and_time();
+#endif
+
+ auto statement = db.prepare(this->body);
+ statement->bind(std::move(this->params));
+
+ while (statement->step() == StepResult::Row)
{
Row<T...> row(this->table_name);
- extract_row_values(row, statement);
+ extract_row_values(row, *statement);
rows.push_back(row);
}
+
return rows;
}
diff --git a/src/database/sqlite3_engine.cpp b/src/database/sqlite3_engine.cpp
new file mode 100644
index 0000000..ae4a146
--- /dev/null
+++ b/src/database/sqlite3_engine.cpp
@@ -0,0 +1,101 @@
+#include <biboumi.h>
+
+#ifdef SQLITE3_FOUND
+
+#include <database/sqlite3_engine.hpp>
+
+#include <database/sqlite3_statement.hpp>
+
+#include <database/query.hpp>
+
+#include <utils/tolower.hpp>
+#include <logger/logger.hpp>
+#include <vector>
+
+Sqlite3Engine::Sqlite3Engine(sqlite3* db):
+ db(db)
+{
+}
+
+Sqlite3Engine::~Sqlite3Engine()
+{
+ sqlite3_close(this->db);
+}
+
+std::set<std::string> Sqlite3Engine::get_all_columns_from_table(const std::string& table_name)
+{
+ std::set<std::string> result;
+ char* errmsg;
+ std::string query{"PRAGMA table_info(" + table_name + ")"};
+ int res = sqlite3_exec(this->db, query.data(), [](void* param, int columns_nb, char** columns, char**) -> int {
+ constexpr int name_column = 1;
+ std::set<std::string>* result = static_cast<std::set<std::string>*>(param);
+ if (name_column < columns_nb)
+ result->insert(utils::tolower(columns[name_column]));
+ return 0;
+ }, &result, &errmsg);
+
+ if (res != SQLITE_OK)
+ {
+ log_error("Error executing ", query, ": ", errmsg);
+ sqlite3_free(errmsg);
+ }
+
+ return result;
+}
+
+std::unique_ptr<DatabaseEngine> Sqlite3Engine::open(const std::string& filename)
+{
+ sqlite3* new_db;
+ auto res = sqlite3_open_v2(filename.data(), &new_db, SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE, nullptr);
+ if (res != SQLITE_OK)
+ {
+ log_error("Failed to open database file ", filename, ": ", sqlite3_errmsg(new_db));
+ sqlite3_close(new_db);
+ throw std::runtime_error("");
+ }
+ return std::make_unique<Sqlite3Engine>(new_db);
+}
+
+std::tuple<bool, std::string> Sqlite3Engine::raw_exec(const std::string& query)
+{
+#ifdef DEBUG_SQL_QUERIES
+ log_debug("SQL QUERY: ", query);
+ const auto timer = make_sql_timer();
+#endif
+
+ char* error;
+ const auto result = sqlite3_exec(db, query.data(), nullptr, nullptr, &error);
+ if (result != SQLITE_OK)
+ {
+ std::string err_msg(error);
+ sqlite3_free(error);
+ return std::make_tuple(false, err_msg);
+ }
+ return std::make_tuple(true, std::string{});
+}
+
+std::unique_ptr<Statement> Sqlite3Engine::prepare(const std::string& query)
+{
+ sqlite3_stmt* stmt;
+ auto res = sqlite3_prepare(db, query.data(), static_cast<int>(query.size()) + 1,
+ &stmt, nullptr);
+ if (res != SQLITE_OK)
+ {
+ log_error("Error preparing statement: ", sqlite3_errmsg(db));
+ return nullptr;
+ }
+ return std::make_unique<Sqlite3Statement>(stmt);
+}
+
+void Sqlite3Engine::extract_last_insert_rowid(Statement&)
+{
+ this->last_inserted_rowid = sqlite3_last_insert_rowid(this->db);
+}
+
+std::string Sqlite3Engine::id_column_type()
+{
+ return "INTEGER PRIMARY KEY AUTOINCREMENT";
+}
+
+#endif
diff --git a/src/database/sqlite3_engine.hpp b/src/database/sqlite3_engine.hpp
new file mode 100644
index 0000000..5b8176c
--- /dev/null
+++ b/src/database/sqlite3_engine.hpp
@@ -0,0 +1,47 @@
+#pragma once
+
+#include <database/engine.hpp>
+
+#include <database/statement.hpp>
+
+#include <memory>
+#include <string>
+#include <tuple>
+#include <set>
+
+#include <biboumi.h>
+
+#ifdef SQLITE3_FOUND
+
+#include <sqlite3.h>
+
+class Sqlite3Engine: public DatabaseEngine
+{
+ public:
+ Sqlite3Engine(sqlite3* db);
+
+ ~Sqlite3Engine();
+
+ static std::unique_ptr<DatabaseEngine> open(const std::string& string);
+
+ std::set<std::string> get_all_columns_from_table(const std::string& table_name) override final;
+ std::tuple<bool, std::string> raw_exec(const std::string& query) override final;
+ std::unique_ptr<Statement> prepare(const std::string& query) override;
+ void extract_last_insert_rowid(Statement& statement) override;
+ std::string id_column_type() override;
+private:
+ sqlite3* const db;
+};
+
+#else
+
+class Sqlite3Engine
+{
+public:
+ static std::unique_ptr<DatabaseEngine> open(const std::string& string)
+ {
+ throw std::runtime_error("Cannot open sqlite3 database "s + string + ": biboumi is not compiled with sqlite3 lib.");
+ }
+};
+
+#endif
diff --git a/src/database/sqlite3_statement.hpp b/src/database/sqlite3_statement.hpp
new file mode 100644
index 0000000..7738fa6
--- /dev/null
+++ b/src/database/sqlite3_statement.hpp
@@ -0,0 +1,92 @@
+#pragma once
+
+#include <database/statement.hpp>
+
+#include <logger/logger.hpp>
+
+#include <sqlite3.h>
+
+class Sqlite3Statement: public Statement
+{
+ public:
+ Sqlite3Statement(sqlite3_stmt* stmt):
+ stmt(stmt) {}
+ ~Sqlite3Statement()
+ {
+ sqlite3_finalize(this->stmt);
+ }
+
+ StepResult step() override final
+ {
+ auto res = sqlite3_step(this->get());
+ if (res == SQLITE_ROW)
+ return StepResult::Row;
+ else if (res == SQLITE_DONE)
+ return StepResult::Done;
+ else
+ return StepResult::Error;
+ }
+
+ void bind(std::vector<std::string> params) override
+ {
+ int i = 1;
+ for (const std::string& param: params)
+ {
+ if (sqlite3_bind_text(this->get(), i, param.data(), static_cast<int>(param.size()), SQLITE_TRANSIENT) != SQLITE_OK)
+ log_error("Failed to bind ", param, " to param ", i);
+ i++;
+ }
+ }
+
+ int64_t get_column_int64(const int col) override
+ {
+ return sqlite3_column_int64(this->get(), col);
+ }
+
+ std::string get_column_text(const int col) override
+ {
+ const auto size = sqlite3_column_bytes(this->get(), col);
+ const unsigned char* str = sqlite3_column_text(this->get(), col);
+ std::string result(reinterpret_cast<const char*>(str), static_cast<std::size_t>(size));
+ return result;
+ }
+
+ bool bind_text(const int pos, const std::string& data) override
+ {
+ return sqlite3_bind_text(this->get(), pos, data.data(), static_cast<int>(data.size()), SQLITE_TRANSIENT) == SQLITE_OK;
+ }
+ bool bind_int64(const int pos, const std::int64_t value) override
+ {
+ return sqlite3_bind_int64(this->get(), pos, static_cast<sqlite3_int64>(value)) == SQLITE_OK;
+ }
+ bool bind_null(const int pos) override
+ {
+ return sqlite3_bind_null(this->get(), pos) == SQLITE_OK;
+ }
+ int get_column_int(const int col) override
+ {
+ return sqlite3_column_int(this->get(), col);
+ }
+
+ Sqlite3Statement(const Sqlite3Statement&) = delete;
+ Sqlite3Statement& operator=(const Sqlite3Statement&) = delete;
+ Sqlite3Statement(Sqlite3Statement&& other):
+ stmt(other.stmt)
+ {
+ other.stmt = nullptr;
+ }
+ Sqlite3Statement& operator=(Sqlite3Statement&& other)
+ {
+ this->stmt = other.stmt;
+ other.stmt = nullptr;
+ return *this;
+ }
+ sqlite3_stmt* get()
+ {
+ return this->stmt;
+ }
+
+ private:
+ sqlite3_stmt* stmt;
+ int last_step_result{SQLITE_OK};
+};
diff --git a/src/database/statement.hpp b/src/database/statement.hpp
index 87cd70f..4a61928 100644
--- a/src/database/statement.hpp
+++ b/src/database/statement.hpp
@@ -1,35 +1,29 @@
#pragma once
-#include <sqlite3.h>
+#include <cstdint>
+#include <string>
+#include <vector>
+
+enum class StepResult
+{
+ Row,
+ Done,
+ Error,
+};
class Statement
{
public:
- Statement(sqlite3_stmt* stmt):
- stmt(stmt) {}
- ~Statement()
- {
- sqlite3_finalize(this->stmt);
- }
+ virtual ~Statement() = default;
+ virtual StepResult step() = 0;
+
+ virtual void bind(std::vector<std::string> params) = 0;
- Statement(const Statement&) = delete;
- Statement& operator=(const Statement&) = delete;
- Statement(Statement&& other):
- stmt(other.stmt)
- {
- other.stmt = nullptr;
- }
- Statement& operator=(Statement&& other)
- {
- this->stmt = other.stmt;
- other.stmt = nullptr;
- return *this;
- }
- sqlite3_stmt* get()
- {
- return this->stmt;
- }
+ virtual std::int64_t get_column_int64(const int col) = 0;
+ virtual std::string get_column_text(const int col) = 0;
+ virtual int get_column_int(const int col) = 0;
- private:
- sqlite3_stmt* stmt;
+ virtual bool bind_text(const int pos, const std::string& data) = 0;
+ virtual bool bind_int64(const int pos, const std::int64_t value) = 0;
+ virtual bool bind_null(const int pos) = 0;
};
diff --git a/src/database/table.cpp b/src/database/table.cpp
deleted file mode 100644
index 9224d79..0000000
--- a/src/database/table.cpp
+++ /dev/null
@@ -1,23 +0,0 @@
-#include <database/table.hpp>
-
-std::set<std::string> get_all_columns_from_table(sqlite3* db, const std::string& table_name)
-{
- std::set<std::string> result;
- char* errmsg;
- std::string query{"PRAGMA table_info(" + table_name + ")"};
- int res = sqlite3_exec(db, query.data(), [](void* param, int columns_nb, char** columns, char**) -> int {
- constexpr int name_column = 1;
- std::set<std::string>* result = static_cast<std::set<std::string>*>(param);
- if (name_column < columns_nb)
- result->insert(columns[name_column]);
- return 0;
- }, &result, &errmsg);
-
- if (res != SQLITE_OK)
- {
- log_error("Error executing ", query, ": ", errmsg);
- sqlite3_free(errmsg);
- }
-
- return result;
-}
diff --git a/src/database/table.hpp b/src/database/table.hpp
index 0060211..680e7cc 100644
--- a/src/database/table.hpp
+++ b/src/database/table.hpp
@@ -1,7 +1,8 @@
#pragma once
+#include <database/engine.hpp>
+
#include <database/select_query.hpp>
-#include <database/type_to_sql.hpp>
#include <database/row.hpp>
#include <algorithm>
@@ -10,23 +11,27 @@
using namespace std::string_literals;
-std::set<std::string> get_all_columns_from_table(sqlite3* db, const std::string& table_name);
+template <typename T>
+std::string ToSQLType(DatabaseEngine& db)
+{
+ if (std::is_same<T, Id>::value)
+ return db.id_column_type();
+ else if (std::is_same<typename T::real_type, std::string>::value)
+ return "TEXT";
+ else
+ return "INTEGER";
+}
template <typename ColumnType>
-void add_column_to_table(sqlite3* db, const std::string& table_name)
+void add_column_to_table(DatabaseEngine& db, const std::string& table_name)
{
const std::string name = ColumnType::name;
- std::string query{"ALTER TABLE " + table_name + " ADD " + ColumnType::name + " " + TypeToSQLType<typename ColumnType::real_type>::type};
- char* error;
- const auto result = sqlite3_exec(db, query.data(), nullptr, nullptr, &error);
- if (result != SQLITE_OK)
- {
- log_error("Error adding column ", name, " to table ", table_name, ": ", error);
- sqlite3_free(error);
- }
+ std::string query{"ALTER TABLE " + table_name + " ADD " + ColumnType::name + " " + ToSQLType<ColumnType>(db)};
+ auto res = db.raw_exec(query);
+ if (std::get<0>(res) == false)
+ log_error("Error adding column ", name, " to table ", table_name, ": ", std::get<1>(res));
}
-
template <typename ColumnType, decltype(ColumnType::options) = nullptr>
void append_option(std::string& s)
{
@@ -50,27 +55,23 @@ class Table
name(std::move(name))
{}
- void upgrade(sqlite3* db)
+ void upgrade(DatabaseEngine& db)
{
- const auto existing_columns = get_all_columns_from_table(db, this->name);
+ const auto existing_columns = db.get_all_columns_from_table(this->name);
add_column_if_not_exists(db, existing_columns);
}
- void create(sqlite3* db)
+ void create(DatabaseEngine& db)
{
- std::string res{"CREATE TABLE IF NOT EXISTS "};
- res += this->name;
- res += " (\n";
- this->add_column_create(res);
- res += ")";
-
- char* error;
- const auto result = sqlite3_exec(db, res.data(), nullptr, nullptr, &error);
- if (result != SQLITE_OK)
- {
- log_error("Error executing query: ", error);
- sqlite3_free(error);
- }
+ std::string query{"CREATE TABLE IF NOT EXISTS "};
+ query += this->name;
+ query += " (";
+ this->add_column_create(db, query);
+ query += ")";
+
+ auto result = db.raw_exec(query);
+ if (std::get<0>(result) == false)
+ log_error("Error executing query: ", std::get<1>(result));
}
RowType row()
@@ -78,7 +79,7 @@ class Table
return {this->name};
}
- SelectQuery<T...> select()
+ auto select()
{
SelectQuery<T...> select(this->name);
return select;
@@ -93,39 +94,34 @@ class Table
template <std::size_t N=0>
typename std::enable_if<N < sizeof...(T), void>::type
- add_column_if_not_exists(sqlite3* db, const std::set<std::string>& existing_columns)
+ add_column_if_not_exists(DatabaseEngine& db, const std::set<std::string>& existing_columns)
{
using ColumnType = typename std::remove_reference<decltype(std::get<N>(std::declval<ColumnTypes>()))>::type;
- if (existing_columns.count(ColumnType::name) != 1)
- {
- add_column_to_table<ColumnType>(db, this->name);
- }
+ if (existing_columns.count(ColumnType::name) == 0)
+ add_column_to_table<ColumnType>(db, this->name);
add_column_if_not_exists<N+1>(db, existing_columns);
}
template <std::size_t N=0>
typename std::enable_if<N == sizeof...(T), void>::type
- add_column_if_not_exists(sqlite3*, const std::set<std::string>&)
+ add_column_if_not_exists(DatabaseEngine&, const std::set<std::string>&)
{}
template <std::size_t N=0>
typename std::enable_if<N < sizeof...(T), void>::type
- add_column_create(std::string& str)
+ add_column_create(DatabaseEngine& db, std::string& str)
{
using ColumnType = typename std::remove_reference<decltype(std::get<N>(std::declval<ColumnTypes>()))>::type;
- using RealType = typename ColumnType::real_type;
str += ColumnType::name;
str += " ";
- str += TypeToSQLType<RealType>::type;
- append_option<ColumnType>(str);
+ str += ToSQLType<ColumnType>(db);
if (N != sizeof...(T) - 1)
str += ",";
- str += "\n";
- add_column_create<N+1>(str);
+ add_column_create<N+1>(db, str);
}
template <std::size_t N=0>
typename std::enable_if<N == sizeof...(T), void>::type
- add_column_create(std::string&)
+ add_column_create(DatabaseEngine&, std::string&)
{ }
const std::string name;
diff --git a/src/database/type_to_sql.cpp b/src/database/type_to_sql.cpp
deleted file mode 100644
index bcd9daa..0000000
--- a/src/database/type_to_sql.cpp
+++ /dev/null
@@ -1,9 +0,0 @@
-#include <database/type_to_sql.hpp>
-
-template <> const std::string TypeToSQLType<int>::type = "INTEGER";
-template <> const std::string TypeToSQLType<std::size_t>::type = "INTEGER";
-template <> const std::string TypeToSQLType<long>::type = "INTEGER";
-template <> const std::string TypeToSQLType<long long>::type = "INTEGER";
-template <> const std::string TypeToSQLType<bool>::type = "INTEGER";
-template <> const std::string TypeToSQLType<std::string>::type = "TEXT";
-template <> const std::string TypeToSQLType<OptionalBool>::type = "INTEGER"; \ No newline at end of file
diff --git a/src/database/type_to_sql.hpp b/src/database/type_to_sql.hpp
deleted file mode 100644
index ba806ab..0000000
--- a/src/database/type_to_sql.hpp
+++ /dev/null
@@ -1,16 +0,0 @@
-#pragma once
-
-#include <utils/optional_bool.hpp>
-
-#include <string>
-
-template <typename T>
-struct TypeToSQLType { static const std::string type; };
-
-template <> const std::string TypeToSQLType<int>::type;
-template <> const std::string TypeToSQLType<std::size_t>::type;
-template <> const std::string TypeToSQLType<long>::type;
-template <> const std::string TypeToSQLType<long long>::type;
-template <> const std::string TypeToSQLType<bool>::type;
-template <> const std::string TypeToSQLType<std::string>::type;
-template <> const std::string TypeToSQLType<OptionalBool>::type; \ No newline at end of file
diff --git a/src/database/update_query.hpp b/src/database/update_query.hpp
new file mode 100644
index 0000000..a29ac3f
--- /dev/null
+++ b/src/database/update_query.hpp
@@ -0,0 +1,104 @@
+#pragma once
+
+#include <database/query.hpp>
+#include <database/engine.hpp>
+
+using namespace std::string_literals;
+
+template <class T, class... Tuple>
+struct Index;
+
+template <class T, class... Types>
+struct Index<T, std::tuple<T, Types...>>
+{
+ static const std::size_t value = 0;
+};
+
+template <class T, class U, class... Types>
+struct Index<T, std::tuple<U, Types...>>
+{
+ static const std::size_t value = Index<T, std::tuple<Types...>>::value + 1;
+};
+
+struct UpdateQuery: public Query
+{
+ template <typename... T>
+ UpdateQuery(const std::string& name, const std::tuple<T...>& columns):
+ Query("UPDATE ")
+ {
+ this->body += name;
+ this->insert_col_names_and_values(columns);
+ }
+
+ template <typename... T>
+ void insert_col_names_and_values(const std::tuple<T...>& columns)
+ {
+ this->body += " SET ";
+ this->insert_col_name_and_value(columns);
+ this->body += " WHERE "s + Id::name + "=$" + std::to_string(this->current_param);
+ }
+
+ template <int N=0, typename... T>
+ typename std::enable_if<N < sizeof...(T), void>::type
+ insert_col_name_and_value(const std::tuple<T...>& columns)
+ {
+ using ColumnType = std::decay_t<decltype(std::get<N>(columns))>;
+
+ if (!std::is_same<ColumnType, Id>::value)
+ {
+ this->body += ColumnType::name + "=$"s + std::to_string(this->current_param);
+ this->current_param++;
+
+ if (N < (sizeof...(T) - 1))
+ this->body += ", ";
+ }
+
+ this->insert_col_name_and_value<N+1>(columns);
+ }
+ template <int N=0, typename... T>
+ typename std::enable_if<N == sizeof...(T), void>::type
+ insert_col_name_and_value(const std::tuple<T...>&)
+ {}
+
+
+ template <typename... T>
+ void execute(DatabaseEngine& db, const std::tuple<T...>& columns)
+ {
+#ifdef DEBUG_SQL_QUERIES
+ const auto timer = this->log_and_time();
+#endif
+
+ auto statement = db.prepare(this->body);
+ this->bind_param(columns, *statement);
+ this->bind_id(columns, *statement);
+
+ statement->step();
+ }
+
+ template <int N=0, typename... T>
+ typename std::enable_if<N < sizeof...(T), void>::type
+ bind_param(const std::tuple<T...>& columns, Statement& statement, int index=1)
+ {
+ auto&& column = std::get<N>(columns);
+ using ColumnType = std::decay_t<decltype(column)>;
+
+ if (!std::is_same<ColumnType, Id>::value)
+ actual_bind(statement, column.value, index++);
+
+ this->bind_param<N+1>(columns, statement, index);
+ }
+
+ template <int N=0, typename... T>
+ typename std::enable_if<N == sizeof...(T), void>::type
+ bind_param(const std::tuple<T...>&, Statement&, int)
+ {}
+
+ template <typename... T>
+ void bind_id(const std::tuple<T...>& columns, Statement& statement)
+ {
+ static constexpr auto index = Index<Id, std::tuple<T...>>::value;
+ auto&& value = std::get<index>(columns);
+
+ actual_bind(statement, value.value, sizeof...(T));
+ }
+};
diff --git a/src/identd/identd_socket.cpp b/src/identd/identd_socket.cpp
index b85257c..92cd80b 100644
--- a/src/identd/identd_socket.cpp
+++ b/src/identd/identd_socket.cpp
@@ -50,14 +50,14 @@ std::string IdentdSocket::generate_answer(const BiboumiComponent& biboumi, uint1
if (pair.second->match_port_pairt(local, remote))
{
std::ostringstream os;
- os << local << " , " << remote << " : USERID : OTHER : " << hash_jid(bridge->get_bare_jid());
+ os << local << " , " << remote << " : USERID : OTHER : " << hash_jid(bridge->get_bare_jid()) << "\r\n";
log_debug("Identd, sending: ", os.str());
return os.str();
}
}
}
std::ostringstream os;
- os << local << " , " << remote << " ERROR : NO-USER";
+ os << local << " , " << remote << " ERROR : NO-USER" << "\r\n";
log_debug("Identd, sending: ", os.str());
return os.str();
}
diff --git a/src/irc/irc_client.cpp b/src/irc/irc_client.cpp
index 46dbdbe..40078d9 100644
--- a/src/irc/irc_client.cpp
+++ b/src/irc/irc_client.cpp
@@ -483,12 +483,16 @@ bool IrcClient::send_channel_message(const std::string& chan_name, const std::st
}
// The max size is 512, taking into account the whole message, not just
// the text we send.
- // This includes our own nick, username and host (because this will be
- // added by the server into our message), in addition to the basic
- // components of the message we send (command name, chan name, \r\n et)
+ // This includes our own nick, constants for username and host (because these
+ // are notoriously hard to know what the server will use), in addition to the basic
+ // components of the message we send (command name, chan name, \r\n etc.)
// : + NICK + ! + USER + @ + HOST + <space> + PRIVMSG + <space> + CHAN + <space> + : + \r\n
+ // 63 is the maximum hostname length defined by the protocol. 10 seems to be
+ // the username limit.
+ constexpr auto max_username_size = 10;
+ constexpr auto max_hostname_size = 63;
const auto line_size = 512 -
- this->current_nick.size() - this->username.size() - this->own_host.size() -
+ this->current_nick.size() - max_username_size - max_hostname_size -
::strlen(":!@ PRIVMSG ") - chan_name.length() - ::strlen(" :\r\n");
const auto lines = cut(body, line_size);
for (const auto& line: lines)
@@ -784,7 +788,7 @@ void IrcClient::on_channel_completely_joined(const IrcMessage& message)
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_room_history(this->hostname, chan_name);
+ this->bridge.send_room_history(this->hostname, chan_name, this->history_limit);
this->bridge.send_topic(this->hostname, chan_name, channel->topic, channel->topic_author);
}
@@ -1017,19 +1021,17 @@ void IrcClient::on_quit(const IrcMessage& message)
const std::string& chan_name = pair.first;
IrcChannel* channel = pair.second.get();
const IrcUser* user = channel->find_user(message.prefix);
+ if (!user)
+ continue;
bool self = false;
if (user == channel->get_self())
self = true;
- if (user)
- {
- std::string nick = user->nick;
- channel->remove_user(user);
- Iid iid;
- iid.set_local(chan_name);
- iid.set_server(this->hostname);
- iid.type = Iid::Type::Channel;
- this->bridge.send_muc_leave(iid, std::move(nick), txt, self, false);
- }
+ Iid iid;
+ iid.set_local(chan_name);
+ iid.set_server(this->hostname);
+ iid.type = Iid::Type::Channel;
+ this->bridge.send_muc_leave(iid, user->nick, txt, self, false);
+ channel->remove_user(user);
}
}
@@ -1073,12 +1075,18 @@ void IrcClient::on_nick(const IrcMessage& message)
void IrcClient::on_kick(const IrcMessage& message)
{
const std::string chan_name = utils::tolower(message.arguments[0]);
- const std::string target = message.arguments[1];
+ const std::string target_nick = message.arguments[1];
const std::string reason = message.arguments[2];
IrcChannel* channel = this->get_channel(chan_name);
if (!channel->joined)
return ;
- const bool self = channel->get_self()->nick == target;
+ const IrcUser* target = channel->find_user(target_nick);
+ if (!target)
+ {
+ log_warning("Received a KICK command from a nick absent from the channel.");
+ return;
+ }
+ const bool self = channel->get_self() == target;
if (self)
channel->joined = false;
IrcUser author(message.prefix);
@@ -1086,7 +1094,8 @@ void IrcClient::on_kick(const IrcMessage& message)
iid.set_local(chan_name);
iid.set_server(this->hostname);
iid.type = Iid::Type::Channel;
- this->bridge.kick_muc_user(std::move(iid), target, reason, author.nick, self);
+ this->bridge.kick_muc_user(std::move(iid), target_nick, reason, author.nick, self);
+ channel->remove_user(target);
}
void IrcClient::on_invite(const IrcMessage& message)
diff --git a/src/irc/irc_client.hpp b/src/irc/irc_client.hpp
index aec6cd9..de5c520 100644
--- a/src/irc/irc_client.hpp
+++ b/src/irc/irc_client.hpp
@@ -5,6 +5,8 @@
#include <irc/irc_channel.hpp>
#include <irc/iid.hpp>
+#include <bridge/history_limit.hpp>
+
#include <network/tcp_client_socket_handler.hpp>
#include <network/resolver.hpp>
@@ -296,6 +298,11 @@ public:
const std::vector<char>& get_sorted_user_modes() const { return this->sorted_user_modes; }
std::set<char> get_chantypes() const { return this->chantypes; }
+
+ /**
+ * Store the history limit that the client asked when joining this room.
+ */
+ HistoryLimit history_limit;
private:
/**
* The hostname of the server we are connected to.
diff --git a/src/logger/logger.cpp b/src/logger/logger.cpp
index 92a3d9b..482cb18 100644
--- a/src/logger/logger.cpp
+++ b/src/logger/logger.cpp
@@ -1,12 +1,35 @@
#include <logger/logger.hpp>
#include <config/config.hpp>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <unistd.h>
+
Logger::Logger(const int log_level):
log_level(log_level),
stream(std::cout.rdbuf()),
null_buffer{},
null_stream{&null_buffer}
{
+#ifdef SYSTEMD_FOUND
+ if (!this->use_stdout())
+ return;
+
+ // See https://www.freedesktop.org/software/systemd/man/systemd.exec.html#%24JOURNAL_STREAM
+ const char* journal_stream = ::getenv("JOURNAL_STREAM");
+ if (journal_stream == nullptr)
+ return;
+
+ struct stat s{};
+ const int res = ::fstat(STDOUT_FILENO, &s);
+ if (res == -1)
+ return;
+
+ const auto stdout_stream = std::to_string(s.st_dev) + ":" + std::to_string(s.st_ino);
+
+ if (stdout_stream == journal_stream)
+ this->use_systemd = true;
+#endif
}
Logger::Logger(const int log_level, const std::string& log_file):
diff --git a/src/logger/logger.hpp b/src/logger/logger.hpp
index ff6a82b..315fc11 100644
--- a/src/logger/logger.hpp
+++ b/src/logger/logger.hpp
@@ -9,8 +9,10 @@
*/
#include <memory>
+#include <string>
#include <iostream>
#include <fstream>
+#include <sstream>
#define debug_lvl 0
#define info_lvl 1
@@ -19,12 +21,18 @@
#include "biboumi.h"
#ifdef SYSTEMD_FOUND
+#define SD_JOURNAL_SUPPRESS_LOCATION
# include <systemd/sd-daemon.h>
+# include <systemd/sd-journal.h>
#else
# define SD_DEBUG "[DEBUG]: "
# define SD_INFO "[INFO]: "
# define SD_WARNING "[WARNING]: "
# define SD_ERR "[ERROR]: "
+# define LOG_ERR 3
+# define LOG_WARNING 4
+# define LOG_INFO 6
+# define LOG_DEBUG 7
#endif
// Macro defined to get the filename instead of the full path. But if it is
@@ -57,8 +65,17 @@ public:
Logger(Logger&&) = delete;
Logger& operator=(Logger&&) = delete;
-private:
+#ifdef SYSTEMD_FOUND
+ bool use_stdout() const
+ {
+ return this->stream.rdbuf() == std::cout.rdbuf();
+ }
+
+ bool use_systemd{false};
+#endif
+
const int log_level;
+private:
std::ofstream ofstream{};
std::ostream stream;
@@ -66,8 +83,6 @@ private:
std::ostream null_stream;
};
-#define WHERE __FILENAME__, ":", __LINE__, ":\t"
-
namespace logging_details
{
template <typename T>
@@ -84,45 +99,41 @@ namespace logging_details
}
template <typename... U>
- void log_debug(U&&... args)
- {
- auto& os = Logger::instance()->get_stream(debug_lvl);
- os << SD_DEBUG;
- log(os, std::forward<U>(args)...);
- }
-
- template <typename... U>
- void log_info(U&&... args)
+ void do_logging(const int level, int syslog_level, const char* src_file, int line, U&&... args)
{
- auto& os = Logger::instance()->get_stream(info_lvl);
- os << SD_INFO;
- log(os, std::forward<U>(args)...);
- }
-
- template <typename... U>
- void log_warning(U&&... args)
- {
- auto& os = Logger::instance()->get_stream(warning_lvl);
- os << SD_WARNING;
- log(os, std::forward<U>(args)...);
- }
-
- template <typename... U>
- void log_error(U&&... args)
- {
- auto& os = Logger::instance()->get_stream(error_lvl);
- os << SD_ERR;
- log(os, std::forward<U>(args)...);
+ #ifdef SYSTEMD_FOUND
+ if (Logger::instance()->use_systemd)
+ {
+ (void)level;
+ if (level >= Logger::instance()->log_level)
+ {
+ std::ostringstream os;
+ log(os, std::forward<U>(args)...);
+ sd_journal_send("MESSAGE=%s", os.str().data(),
+ "PRIORITY=%i", syslog_level,
+ "CODE_FILE=%s", src_file,
+ "CODE_LINE=%i", line,
+ nullptr);
+ }
+ }
+ else
+ {
+ #endif
+ (void)syslog_level;
+ static const char* priority_names[] = {"DEBUG", "INFO", "WARNING", "ERROR"};
+ auto& os = Logger::instance()->get_stream(level);
+ os << '[' << priority_names[level] << "]: " << src_file << ':' << line << ":\t";
+ log(os, std::forward<U>(args)...);
+#ifdef SYSTEMD_FOUND
+ }
+#endif
}
}
-#define log_info(...) logging_details::log_info(WHERE, __VA_ARGS__)
-
-#define log_warning(...) logging_details::log_warning(WHERE, __VA_ARGS__)
-
-#define log_error(...) logging_details::log_error(WHERE, __VA_ARGS__)
-
-#define log_debug(...) logging_details::log_debug(WHERE, __VA_ARGS__)
+#define log_debug(...) logging_details::do_logging(debug_lvl, LOG_DEBUG, __FILENAME__, __LINE__, __VA_ARGS__)
+#define log_info(...) logging_details::do_logging(info_lvl, LOG_INFO, __FILENAME__, __LINE__, __VA_ARGS__)
+#define log_warning(...) logging_details::do_logging(warning_lvl, LOG_WARNING, __FILENAME__, __LINE__, __VA_ARGS__)
+#define log_error(...) logging_details::do_logging(error_lvl, LOG_ERR, __FILENAME__, __LINE__, __VA_ARGS__)
diff --git a/src/main.cpp b/src/main.cpp
index 5725584..c877e43 100644
--- a/src/main.cpp
+++ b/src/main.cpp
@@ -88,7 +88,8 @@ int main(int ac, char** av)
#ifdef USE_DATABASE
try {
open_database();
- } catch (...) {
+ } catch (const std::exception& e) {
+ log_error(e.what());
return 1;
}
#endif
diff --git a/src/network/credentials_manager.cpp b/src/network/credentials_manager.cpp
index 7f07cef..b25f442 100644
--- a/src/network/credentials_manager.cpp
+++ b/src/network/credentials_manager.cpp
@@ -5,6 +5,7 @@
#include <network/credentials_manager.hpp>
#include <logger/logger.hpp>
#include <botan/tls_exceptn.h>
+#include <botan/data_src.h>
#include <config/config.hpp>
/**
diff --git a/src/network/credentials_manager.hpp b/src/network/credentials_manager.hpp
index aa4732a..3a37bdc 100644
--- a/src/network/credentials_manager.hpp
+++ b/src/network/credentials_manager.hpp
@@ -4,7 +4,8 @@
#ifdef BOTAN_FOUND
-#include <botan/botan.h>
+#include <botan/credentials_manager.h>
+#include <botan/certstor.h>
#include <botan/tls_client.h>
class TCPSocketHandler;
diff --git a/src/network/tcp_socket_handler.cpp b/src/network/tcp_socket_handler.cpp
index 6239162..642cf03 100644
--- a/src/network/tcp_socket_handler.cpp
+++ b/src/network/tcp_socket_handler.cpp
@@ -12,7 +12,9 @@
#include <cstring>
#ifdef BOTAN_FOUND
+# include <botan/version.h>
# include <botan/hex.h>
+# include <botan/auto_rng.h>
# include <botan/tls_exceptn.h>
# include <config/config.hpp>
# include <utils/dirname.hpp>
@@ -27,6 +29,10 @@ namespace
Botan::TLS::Session_Manager_In_Memory& get_session_manager()
{
static Botan::TLS::Session_Manager_In_Memory session_manager{get_rng()};
+#if BOTAN_VERSION_CODE < BOTAN_VERSION_CODE_FOR(2,4,0)
+ // workaround for https://github.com/randombit/botan/issues/1276
+ session_manager.remove_all();
+#endif
return session_manager;
}
}
diff --git a/src/network/tcp_socket_handler.hpp b/src/network/tcp_socket_handler.hpp
index 5cef739..c598641 100644
--- a/src/network/tcp_socket_handler.hpp
+++ b/src/network/tcp_socket_handler.hpp
@@ -21,7 +21,6 @@
#ifdef BOTAN_FOUND
# include <botan/types.h>
-# include <botan/botan.h>
# include <botan/tls_session_manager.h>
# include <network/tls_policy.hpp>
diff --git a/src/network/tls_policy.cpp b/src/network/tls_policy.cpp
index 5439397..b88eb88 100644
--- a/src/network/tls_policy.cpp
+++ b/src/network/tls_policy.cpp
@@ -8,6 +8,8 @@
#include <network/tls_policy.hpp>
#include <logger/logger.hpp>
+#include <botan/parsing.h>
+#include <botan/exceptn.h>
bool BiboumiTLSPolicy::load(const std::string& filename)
{
diff --git a/src/utils/is_one_of.hpp b/src/utils/is_one_of.hpp
new file mode 100644
index 0000000..4d6770e
--- /dev/null
+++ b/src/utils/is_one_of.hpp
@@ -0,0 +1,17 @@
+#pragma once
+
+#include <type_traits>
+
+template <typename...>
+struct is_one_of_implem {
+ static constexpr bool value = false;
+};
+
+template <typename F, typename S, typename... T>
+struct is_one_of_implem<F, S, T...> {
+ static constexpr bool value =
+ std::is_same<F, S>::value || is_one_of_implem<F, T...>::value;
+};
+
+template<typename... T>
+constexpr bool is_one_of = is_one_of_implem<T...>::value;
diff --git a/src/utils/optional_bool.cpp b/src/utils/optional_bool.cpp
new file mode 100644
index 0000000..56fdca2
--- /dev/null
+++ b/src/utils/optional_bool.cpp
@@ -0,0 +1,8 @@
+#include <utils/optional_bool.hpp>
+
+
+std::ostream& operator<<(std::ostream& os, const OptionalBool& o)
+{
+ os << o.to_string();
+ return os;
+}
diff --git a/src/utils/optional_bool.hpp b/src/utils/optional_bool.hpp
index 59bbbab..867aca2 100644
--- a/src/utils/optional_bool.hpp
+++ b/src/utils/optional_bool.hpp
@@ -20,7 +20,7 @@ struct OptionalBool
this->is_set = false;
}
- std::string to_string()
+ std::string to_string() const
{
if (this->is_set == false)
return "unset";
@@ -33,3 +33,5 @@ struct OptionalBool
bool is_set{false};
bool value{false};
};
+
+std::ostream& operator<<(std::ostream& os, const OptionalBool& o);
diff --git a/src/utils/scopetimer.hpp b/src/utils/scopetimer.hpp
new file mode 100644
index 0000000..7d3db9b
--- /dev/null
+++ b/src/utils/scopetimer.hpp
@@ -0,0 +1,17 @@
+#include <utils/scopeguard.hpp>
+
+#include <chrono>
+
+#include <logger/logger.hpp>
+
+template <typename Callback>
+auto make_scope_timer(Callback cb)
+{
+ const auto start_time = std::chrono::steady_clock::now();
+ return utils::make_scope_guard([start_time, cb = std::move(cb)]()
+ {
+ const auto now = std::chrono::steady_clock::now();
+ const auto elapsed = now - start_time;
+ cb(elapsed);
+ });
+}
diff --git a/src/utils/time.cpp b/src/utils/time.cpp
index bc2c18d..71306fd 100644
--- a/src/utils/time.cpp
+++ b/src/utils/time.cpp
@@ -9,9 +9,10 @@
namespace utils
{
-std::string to_string(const std::time_t& timestamp)
+std::string to_string(const std::chrono::system_clock::time_point::rep& time)
{
constexpr std::size_t stamp_size = 21;
+ const std::time_t timestamp = static_cast<std::time_t>(time);
char date_buf[stamp_size];
if (std::strftime(date_buf, stamp_size, "%FT%TZ", std::gmtime(&timestamp)) != stamp_size - 1)
return "";
diff --git a/src/utils/time.hpp b/src/utils/time.hpp
index c71cd9c..4b19634 100644
--- a/src/utils/time.hpp
+++ b/src/utils/time.hpp
@@ -2,9 +2,10 @@
#include <ctime>
#include <string>
+#include <chrono>
namespace utils
{
-std::string to_string(const std::time_t& timestamp);
+std::string to_string(const std::chrono::system_clock::time_point::rep& timestamp);
std::time_t parse_datetime(const std::string& stamp);
-} \ No newline at end of file
+}
diff --git a/src/xmpp/adhoc_commands_handler.cpp b/src/xmpp/adhoc_commands_handler.cpp
index e4dcd5c..bb48781 100644
--- a/src/xmpp/adhoc_commands_handler.cpp
+++ b/src/xmpp/adhoc_commands_handler.cpp
@@ -83,7 +83,7 @@ XmlNode AdhocCommandsHandler::handle_request(const std::string& executor_jid, co
XmlSubNode next(actions, "next");
}
}
- else if (action == "cancel")
+ else if (session_it != this->sessions.end() && action == "cancel")
{
this->sessions.erase(session_it);
command_node["status"] = "canceled";
diff --git a/src/xmpp/biboumi_adhoc_commands.cpp b/src/xmpp/biboumi_adhoc_commands.cpp
index 60af506..bcdac39 100644
--- a/src/xmpp/biboumi_adhoc_commands.cpp
+++ b/src/xmpp/biboumi_adhoc_commands.cpp
@@ -159,7 +159,7 @@ void ConfigureGlobalStep1(XmppComponent&, AdhocSession& session, XmlNode& comman
{
XmlSubNode value(persistent, "value");
value.set_name("value");
- if (options.col<Database::Persistent>())
+ if (options.col<Database::GlobalPersistent>())
value.set_inner("true");
else
value.set_inner("false");
@@ -193,7 +193,7 @@ void ConfigureGlobalStep2(XmppComponent& xmpp_component, AdhocSession& session,
}
else if (field->get_tag("var") == "persistent" &&
value)
- options.col<Database::Persistent>() = to_bool(value->get_inner());
+ options.col<Database::GlobalPersistent>() = to_bool(value->get_inner());
}
options.save(Database::db);
@@ -409,7 +409,7 @@ void ConfigureIrcServerStep2(XmppComponent&, AdhocSession& session, XmlNode& com
else if (field->get_tag("var") == "pass" && value)
options.col<Database::Pass>() = value->get_inner();
- else if (field->get_tag("var") == "after_connect_command")
+ else if (field->get_tag("var") == "after_connect_command" && value)
options.col<Database::AfterConnectionCommand>() = value->get_inner();
else if (field->get_tag("var") == "username" && value)
@@ -430,7 +430,7 @@ void ConfigureIrcServerStep2(XmppComponent&, AdhocSession& session, XmlNode& com
options.col<Database::EncodingIn>() = value->get_inner();
}
-
+ Database::invalidate_encoding_in_cache();
options.save(Database::db);
command_node.delete_all_children();
@@ -599,7 +599,7 @@ bool handle_irc_channel_configuration_form(XmppComponent& xmpp_component, const
}
}
-
+ Database::invalidate_encoding_in_cache(requester.bare(), iid.get_server(), iid.get_local());
options.save(Database::db);
}
return true;
diff --git a/src/xmpp/biboumi_component.cpp b/src/xmpp/biboumi_component.cpp
index 0e1d270..481ebb9 100644
--- a/src/xmpp/biboumi_component.cpp
+++ b/src/xmpp/biboumi_component.cpp
@@ -7,6 +7,7 @@
#include <xmpp/adhoc_command.hpp>
#include <xmpp/biboumi_adhoc_commands.hpp>
#include <bridge/list_element.hpp>
+#include <utils/encoding.hpp>
#include <config/config.hpp>
#include <utils/time.hpp>
#include <xmpp/jid.hpp>
@@ -24,6 +25,7 @@
#include <database/database.hpp>
#include <bridge/result_set_management.hpp>
+#include <bridge/history_limit.hpp>
using namespace std::string_literals;
@@ -155,8 +157,32 @@ void BiboumiComponent::handle_presence(const Stanza& stanza)
bridge->send_irc_nick_change(iid, to.resource, from.resource);
const XmlNode* x = stanza.get_child("x", MUC_NS);
const XmlNode* password = x ? x->get_child("password", MUC_NS): nullptr;
+ const XmlNode* history = x ? x->get_child("history", MUC_NS): nullptr;
+ HistoryLimit history_limit;
+ if (history)
+ {
+ const auto seconds = history->get_tag("seconds");
+ if (!seconds.empty())
+ {
+ const auto now = std::chrono::system_clock::now();
+ std::time_t timestamp = std::chrono::system_clock::to_time_t(now);
+ int int_seconds = std::atoi(seconds.data());
+ timestamp -= int_seconds;
+ history_limit.since = utils::to_string(timestamp);
+ }
+ const auto since = history->get_tag("since");
+ if (!since.empty())
+ history_limit.since = since;
+ const auto maxstanzas = history->get_tag("maxstanzas");
+ if (!maxstanzas.empty())
+ history_limit.stanzas = std::atoi(maxstanzas.data());
+ // Ignore any other value, because this is too complex to implement,
+ // so I won’t do it.
+ if (history->get_tag("maxchars") == "0")
+ history_limit.stanzas = 0;
+ }
bridge->join_irc_channel(iid, to.resource, password ? password->get_inner(): "",
- from.resource);
+ from.resource, history_limit);
}
else if (type == "unavailable")
{
@@ -281,6 +307,7 @@ void BiboumiComponent::handle_message(const Stanza& stanza)
{
if (body && !body->get_inner().empty())
{
+ const auto fixed_irc_server = Config::get("fixed_irc_server", "");
// a message for nick!server
if (iid.type == Iid::Type::User && !iid.get_local().empty())
{
@@ -296,9 +323,11 @@ void BiboumiComponent::handle_message(const Stanza& stanza)
bridge->set_preferred_from_jid(user_iid.get_local(), to_str);
}
else if (iid.type == Iid::Type::Server)
+ bridge->send_raw_message(iid.get_server(), body->get_inner());
+ else if (iid.type == Iid::Type::None && !fixed_irc_server.empty())
{ // 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());
+ bridge->send_raw_message(fixed_irc_server, body->get_inner());
}
}
}
@@ -408,7 +437,7 @@ 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())
adhoc_handler = &this->adhoc_commands_handler;
@@ -416,8 +445,13 @@ void BiboumiComponent::handle_iq(const Stanza& stanza)
{
if (iid.type == Iid::Type::Server)
adhoc_handler = &this->irc_server_adhoc_commands_handler;
- else
+ else if (iid.type == Iid::Type::Channel && to.resource.empty())
adhoc_handler = &this->irc_channel_adhoc_commands_handler;
+ else
+ {
+ error_name = "feature-not-implemented";
+ return;
+ }
}
// Execute the command, if any, and get a result XmlNode that we
// insert in our response
@@ -467,7 +501,7 @@ void BiboumiComponent::handle_iq(const Stanza& stanza)
stanza_error.disable();
}
}
- else if (iid.type == Iid::Type::Channel)
+ else if (iid.type == Iid::Type::Channel && to.resource.empty())
{
if (node.empty())
{
@@ -526,7 +560,7 @@ void BiboumiComponent::handle_iq(const Stanza& stanza)
this->irc_server_adhoc_commands_handler);
stanza_error.disable();
}
- else if (iid.type == Iid::Type::Channel)
+ else if (iid.type == Iid::Type::Channel && to.resource.empty())
{ // Get the channel's adhoc commands
this->send_adhoc_commands_list(id, from, to_str,
(Config::get("admin", "") ==
@@ -534,6 +568,8 @@ void BiboumiComponent::handle_iq(const Stanza& stanza)
this->irc_channel_adhoc_commands_handler);
stanza_error.disable();
}
+ else // “to” is a MUC user, not the room itself
+ error_name = "feature-not-implemented";
}
else if (node.empty() && iid.type == Iid::Type::Server)
{ // Disco on an IRC server: get the list of channels
@@ -685,19 +721,46 @@ bool BiboumiComponent::handle_mam_request(const Stanza& stanza)
if (max)
limit = std::atoi(max->get_inner().data());
}
- // If the archive is really big, and the client didn’t specify any
- // limit, we avoid flooding it: we set an arbitrary max limit.
- if (limit == -1 && start.empty() && end.empty())
+ // Do send more than 100 messages, even if the client asked for more,
+ // or if it didn’t specify any limit.
+ // 101 is just a trick to know if there are more available messages.
+ // If our query returns 101 message, we know it’s incomplete, but we
+ // still send only 100
+ if ((limit == -1 && start.empty() && end.empty())
+ || limit > 100)
+ limit = 101;
+ auto lines = Database::get_muc_logs(from.bare(), iid.get_local(), iid.get_server(), limit, start, end);
+ bool complete = true;
+ if (lines.size() > 100)
{
- limit = 100;
+ complete = false;
+ lines.erase(lines.begin(), std::prev(lines.end(), 100));
}
- const auto lines = Database::get_muc_logs(from.bare(), iid.get_local(), iid.get_server(), limit, start, end);
for (const Database::MucLogLine& line: lines)
{
if (!line.col<Database::Nick>().empty())
this->send_archived_message(line, to.full(), from.full(), query_id);
}
- this->send_iq_result_full_jid(id, from.full(), to.full());
+ {
+ auto fin_ptr = std::make_unique<XmlNode>("fin");
+ {
+ XmlNode& fin = *(fin_ptr.get());
+ fin["xmlns"] = MAM_NS;
+ if (complete)
+ fin["complete"] = "true";
+ XmlSubNode set(fin, "set");
+ set["xmlns"] = RSM_NS;
+ if (!lines.empty())
+ {
+ XmlSubNode first(set, "first");
+ first["index"] = "0";
+ first.set_inner(lines[0].col<Database::Uuid>());
+ XmlSubNode last(set, "last");
+ last.set_inner(lines[lines.size() - 1].col<Database::Uuid>());
+ }
+ }
+ this->send_iq_result_full_jid(id, from.full(), to.full(), std::move(fin_ptr));
+ }
return true;
}
return false;
@@ -739,7 +802,7 @@ bool BiboumiComponent::handle_room_configuration_form_request(const std::string&
{
Iid iid(to.local, {'#', '&'});
- if (iid.type != Iid::Type::Channel)
+ if (iid.type != Iid::Type::Channel || !to.resource.empty())
return false;
Stanza iq("iq");
@@ -761,7 +824,7 @@ bool BiboumiComponent::handle_room_configuration_form(const XmlNode& query, cons
{
Iid iid(to.local, {'#', '&'});
- if (iid.type != Iid::Type::Channel)
+ if (iid.type != Iid::Type::Channel || !to.resource.empty())
return false;
Jid requester(from);
@@ -958,7 +1021,9 @@ void BiboumiComponent::send_iq_room_list_result(const std::string& id, const std
for (auto it = begin; it != end; ++it)
{
XmlSubNode item(query, "item");
- item["jid"] = it->channel + "@" + this->served_hostname;
+ std::string channel_name = it->channel;
+ xep0106::encode(channel_name);
+ item["jid"] = channel_name + "@" + this->served_hostname;
}
if ((rs_info.max >= 0 || !rs_info.after.empty() || !rs_info.before.empty()))
@@ -1052,6 +1117,9 @@ void BiboumiComponent::on_irc_client_connected(const std::string& irc_hostname,
const auto local_jid = irc_hostname + "@" + this->served_hostname;
if (Database::has_roster_item(local_jid, jid))
this->send_presence_to_contact(local_jid, jid, "");
+#else
+ (void)irc_hostname;
+ (void)jid;
#endif
}
@@ -1061,6 +1129,9 @@ void BiboumiComponent::on_irc_client_disconnected(const std::string& irc_hostnam
const auto local_jid = irc_hostname + "@" + this->served_hostname;
if (Database::has_roster_item(local_jid, jid))
this->send_presence_to_contact(irc_hostname + "@" + this->served_hostname, jid, "unavailable");
+#else
+ (void)irc_hostname;
+ (void)jid;
#endif
}
diff --git a/src/xmpp/body.hpp b/src/xmpp/body.hpp
index 068d1a4..f693cdd 100644
--- a/src/xmpp/body.hpp
+++ b/src/xmpp/body.hpp
@@ -1,5 +1,9 @@
#pragma once
+#include <tuple>
+#include <memory>
+
+class XmlNode;
namespace Xmpp
{
diff --git a/src/xmpp/xmpp_component.cpp b/src/xmpp/xmpp_component.cpp
index 42a5392..9be9e34 100644
--- a/src/xmpp/xmpp_component.cpp
+++ b/src/xmpp/xmpp_component.cpp
@@ -269,7 +269,8 @@ void* XmppComponent::get_receive_buffer(const size_t size) const
}
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)
+ const std::string& type, const bool fulljid, const bool nocopy,
+ const bool muc_private)
{
Stanza message("message");
{
@@ -277,7 +278,12 @@ void XmppComponent::send_message(const std::string& from, Xmpp::body&& body, con
if (fulljid)
message["from"] = from;
else
- message["from"] = from + "@" + this->served_hostname;
+ {
+ if (!from.empty())
+ message["from"] = from + "@" + this->served_hostname;
+ else
+ message["from"] = this->served_hostname;
+ }
if (!type.empty())
message["type"] = type;
XmlSubNode body_node(message, "body");
@@ -296,6 +302,11 @@ void XmppComponent::send_message(const std::string& from, Xmpp::body&& body, con
XmlSubNode nocopy(message, "no-copy");
nocopy["xmlns"] = "urn:xmpp:hints";
}
+ if (muc_private)
+ {
+ XmlSubNode x(message, "x");
+ x["xmlns"] = MUC_USER_NS;
+ }
}
this->send_stanza(message);
}
@@ -387,7 +398,8 @@ 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)
+#ifdef USE_DATABASE
+void XmppComponent::send_history_message(const std::string& muc_name, const std::string& nick, const std::string& body_txt, const std::string& jid_to, Database::time_point::rep timestamp)
{
Stanza message("message");
message["to"] = jid_to;
@@ -410,6 +422,7 @@ void XmppComponent::send_history_message(const std::string& muc_name, const std:
this->send_stanza(message);
}
+#endif
void XmppComponent::send_muc_leave(const std::string& muc_name, const std::string& nick, Xmpp::body&& message,
const std::string& jid_to, const bool self, const bool user_requested)
@@ -629,13 +642,15 @@ void XmppComponent::send_iq_version_request(const std::string& from,
this->send_stanza(iq);
}
-void XmppComponent::send_iq_result_full_jid(const std::string& id, const std::string& to_jid, const std::string& from_full_jid)
+void XmppComponent::send_iq_result_full_jid(const std::string& id, const std::string& to_jid, const std::string& from_full_jid, std::unique_ptr<XmlNode> inner)
{
Stanza iq("iq");
iq["from"] = from_full_jid;
iq["to"] = to_jid;
iq["id"] = id;
iq["type"] = "result";
+ if (inner)
+ iq.add_child(std::move(inner));
this->send_stanza(iq);
}
diff --git a/src/xmpp/xmpp_component.hpp b/src/xmpp/xmpp_component.hpp
index 22d5c48..1daa6fb 100644
--- a/src/xmpp/xmpp_component.hpp
+++ b/src/xmpp/xmpp_component.hpp
@@ -1,8 +1,10 @@
#pragma once
+#include "biboumi.h"
#include <xmpp/adhoc_commands_handler.hpp>
#include <network/tcp_client_socket_handler.hpp>
+#include <database/database.hpp>
#include <xmpp/xmpp_parser.hpp>
#include <xmpp/body.hpp>
@@ -112,7 +114,8 @@ public:
* 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, const bool nocopy=false);
+ const std::string& type, const bool fulljid, const bool nocopy=false,
+ const bool muc_private=false);
/**
* Send a join from a new participant
*/
@@ -132,11 +135,13 @@ public:
*/
void send_muc_message(const std::string& muc_name, const std::string& nick, Xmpp::body&& body, const std::string& jid_to,
std::string uuid);
+#ifdef USE_DATABASE
/**
* 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);
+ const std::string& jid_to, Database::time_point::rep timestamp);
+#endif
/**
* Send an unavailable presence for this nick
*/
@@ -202,7 +207,7 @@ public:
*/
void send_iq_result(const std::string& id, const std::string& to_jid, const std::string& from);
void send_iq_result_full_jid(const std::string& id, const std::string& to_jid,
- const std::string& from_full_jid);
+ const std::string& from_full_jid, std::unique_ptr<XmlNode> inner=nullptr);
void handle_handshake(const Stanza& stanza);
void handle_error(const Stanza& stanza);
diff --git a/tests/database.cpp b/tests/database.cpp
index f49220a..7ab6da8 100644
--- a/tests/database.cpp
+++ b/tests/database.cpp
@@ -1,19 +1,43 @@
#include "catch.hpp"
+#include <biboumi.h>
+
+#ifdef USE_DATABASE
+
+#include <cstdlib>
+
#include <database/database.hpp>
#include <config/config.hpp>
TEST_CASE("Database")
{
-#ifdef USE_DATABASE
- Database::open(":memory:");
+#ifdef PQ_FOUND
+ std::string postgresql_uri{"postgresql://"};
+ const char* env_value = ::getenv("TEST_POSTGRES_URI");
+ if (env_value != nullptr)
+ Database::open("postgresql://"s + env_value);
+ else
+#endif
+ Database::open(":memory:");
+
+ Database::raw_exec("DELETE FROM " + Database::irc_server_options.get_name());
+ Database::raw_exec("DELETE FROM " + Database::irc_channel_options.get_name());
SECTION("Basic retrieve and update")
{
auto o = Database::get_irc_server_options("zouzou@example.com", "irc.example.com");
+ CHECK(Database::count(Database::irc_server_options) == 0);
+ o.save(Database::db);
+ CHECK(Database::count(Database::irc_server_options) == 1);
+ o.col<Database::Realname>() = "Different realname";
+ CHECK(o.col<Database::Realname>() == "Different realname");
o.save(Database::db);
+ CHECK(o.col<Database::Realname>() == "Different realname");
+ CHECK(Database::count(Database::irc_server_options) == 1);
+
auto a = Database::get_irc_server_options("zouzou@example.com", "irc.example.com");
+ CHECK(a.col<Database::Realname>() == "Different realname");
auto b = Database::get_irc_server_options("moumou@example.com", "irc.example.com");
// b does not yet exist in the db, the object is created but not yet
@@ -28,7 +52,6 @@ TEST_CASE("Database")
SECTION("channel options")
{
- Config::set("db_name", ":memory:");
auto o = Database::get_irc_channel_options("zouzou@example.com", "irc.example.com", "#foo");
CHECK(o.col<Database::EncodingIn>() == "");
@@ -95,5 +118,5 @@ TEST_CASE("Database")
}
Database::close();
-#endif
}
+#endif
diff --git a/tests/end_to_end/__main__.py b/tests/end_to_end/__main__.py
index c6607d0..c4c149a 100644
--- a/tests/end_to_end/__main__.py
+++ b/tests/end_to_end/__main__.py
@@ -130,6 +130,7 @@ def match(stanza, xpath):
'dataform': 'jabber:x:data',
'version': 'jabber:iq:version',
'mam': 'urn:xmpp:mam:2',
+ 'rms': 'http://jabber.org/protocol/rsm',
'delay': 'urn:xmpp:delay',
'forward': 'urn:xmpp:forward:0',
'client': 'jabber:client',
@@ -377,7 +378,15 @@ port=8811
fixed_irc_server=irc.localhost
admin=admin@example.com
identd_port=1113
-"""}
+""",
+
+'persistent_by_default':
+"""hostname=biboumi.localhost
+password=coucou
+db_name=e2e_test.sqlite
+port=8811
+persistent_by_default=true
+""",}
common_replacements = {
'irc_server_one': 'irc.localhost@biboumi.localhost',
@@ -403,13 +412,21 @@ def handshake_sequence():
partial(send_stanza, "<handshake xmlns='jabber:component:accept'/>"))
-def connection_begin_sequence(irc_host, jid, expected_irc_presence=False):
+def connection_begin_sequence(irc_host, jid, expected_irc_presence=False, fixed_irc_server=False):
jid = jid.format_map(common_replacements)
- xpath = "/message[@to='" + jid + "'][@from='" + irc_host + "@biboumi.localhost']/body[text()='%s']"
- xpath_re = "/message[@to='" + jid + "'][@from='" + irc_host + "@biboumi.localhost']/body[re:test(text(), '%s')]"
+ if fixed_irc_server:
+ xpath = "/message[@to='" + jid + "'][@from='biboumi.localhost']/body[text()='%s']"
+ xpath_re = "/message[@to='" + jid + "'][@from='biboumi.localhost']/body[re:test(text(), '%s')]"
+ else:
+ xpath = "/message[@to='" + jid + "'][@from='" + irc_host + "@biboumi.localhost']/body[text()='%s']"
+ xpath_re = "/message[@to='" + jid + "'][@from='" + irc_host + "@biboumi.localhost']/body[re:test(text(), '%s')]"
result = (
partial(expect_stanza,
- xpath % ('Connecting to %s:6697 (encrypted)' % irc_host)),
+ (xpath % ('Connecting to %s:6697 (encrypted)' % irc_host),
+ "/message/hints:no-copy",
+ "/message/carbon:private"
+ )
+ ),
partial(expect_stanza,
xpath % 'Connection failed: Connection refused'),
partial(expect_stanza,
@@ -440,14 +457,22 @@ def connection_begin_sequence(irc_host, jid, expected_irc_presence=False):
return result
-def connection_tls_begin_sequence(irc_host, jid):
+def connection_tls_begin_sequence(irc_host, jid, fixed_irc_server):
jid = jid.format_map(common_replacements)
- xpath = "/message[@to='" + jid + "'][@from='" + irc_host + "@biboumi.localhost']/body[text()='%s']"
- xpath_re = "/message[@to='" + jid + "'][@from='" + irc_host + "@biboumi.localhost']/body[re:test(text(), '%s')]"
+ if fixed_irc_server:
+ xpath = "/message[@to='" + jid + "'][@from='biboumi.localhost']/body[text()='%s']"
+ xpath_re = "/message[@to='" + jid + "'][@from='biboumi.localhost']/body[re:test(text(), '%s')]"
+ else:
+ xpath = "/message[@to='" + jid + "'][@from='" + irc_host + "@biboumi.localhost']/body[text()='%s']"
+ xpath_re = "/message[@to='" + jid + "'][@from='" + irc_host + "@biboumi.localhost']/body[re:test(text(), '%s')]"
irc_host = 'irc.localhost'
return (
partial(expect_stanza,
- xpath % ('Connecting to %s:7778 (encrypted)' % irc_host)),
+ (xpath % ('Connecting to %s:7778 (encrypted)' % irc_host),
+ "/message/hints:no-copy",
+ "/message/carbon:private",
+ )
+ ),
partial(expect_stanza,
xpath % 'Connected to IRC server (encrypted).'),
# These five messages can be receive in any order
@@ -463,10 +488,14 @@ def connection_tls_begin_sequence(irc_host, jid):
xpath_re % (r'^%s: (\*\*\* Checking Ident|\*\*\* Looking up your hostname\.\.\.|\*\*\* Found your hostname: .*|ACK multi-prefix|\*\*\* Got Ident response)$' % irc_host)),
)
-def connection_end_sequence(irc_host, jid):
+def connection_end_sequence(irc_host, jid, fixed_irc_server=False):
jid = jid.format_map(common_replacements)
- xpath = "/message[@to='" + jid + "'][@from='" + irc_host + "@biboumi.localhost']/body[text()='%s']"
- xpath_re = "/message[@to='" + jid + "'][@from='" + irc_host + "@biboumi.localhost']/body[re:test(text(), '%s')]"
+ if fixed_irc_server:
+ xpath = "/message[@to='" + jid + "'][@from='biboumi.localhost']/body[text()='%s']"
+ xpath_re = "/message[@to='" + jid + "'][@from='biboumi.localhost']/body[re:test(text(), '%s')]"
+ else:
+ xpath = "/message[@to='" + jid + "'][@from='" + irc_host + "@biboumi.localhost']/body[text()='%s']"
+ xpath_re = "/message[@to='" + jid + "'][@from='" + irc_host + "@biboumi.localhost']/body[re:test(text(), '%s')]"
irc_host = 'irc.localhost'
return (
partial(expect_stanza,
@@ -493,23 +522,26 @@ def connection_end_sequence(irc_host, jid):
xpath_re % r'^User mode for \w+ is \[\+Z?i\]$'),
)
-def connection_middle_sequence(irc_host, jid):
- xpath_re = "/message[@to='" + jid + "'][@from='" + irc_host + "@biboumi.localhost']/body[re:test(text(), '%s')]"
+def connection_middle_sequence(irc_host, jid, fixed_irc_server=False):
+ if fixed_irc_server:
+ xpath_re = "/message[@to='" + jid + "'][@from='biboumi.localhost']/body[re:test(text(), '%s')]"
+ else:
+ xpath_re = "/message[@to='" + jid + "'][@from='" + irc_host + "@biboumi.localhost']/body[re:test(text(), '%s')]"
irc_host = 'irc.localhost'
return (
partial(expect_stanza, xpath_re % (r'^%s: \*\*\* You are exempt from flood limits$' % irc_host)),
)
-def connection_sequence(irc_host, jid, expected_irc_presence=False):
- return connection_begin_sequence(irc_host, jid, expected_irc_presence) +\
- connection_middle_sequence(irc_host, jid) +\
- connection_end_sequence(irc_host, jid)
+def connection_sequence(irc_host, jid, expected_irc_presence=False, fixed_irc_server=False):
+ return connection_begin_sequence(irc_host, jid, expected_irc_presence, fixed_irc_server=fixed_irc_server) +\
+ connection_middle_sequence(irc_host, jid, fixed_irc_server=fixed_irc_server) +\
+ connection_end_sequence(irc_host, jid, fixed_irc_server=fixed_irc_server)
-def connection_tls_sequence(irc_host, jid):
- return connection_tls_begin_sequence(irc_host, jid) + \
- connection_middle_sequence(irc_host, jid) +\
- connection_end_sequence(irc_host, jid)
+def connection_tls_sequence(irc_host, jid, fixed_irc_server=False):
+ return connection_tls_begin_sequence(irc_host, jid, fixed_irc_server) + \
+ connection_middle_sequence(irc_host, jid, fixed_irc_server) +\
+ connection_end_sequence(irc_host, jid, fixed_irc_server)
def extract_attribute(xpath, name, stanza):
@@ -592,8 +624,6 @@ if __name__ == '__main__':
partial(send_stanza, "<message from='{jid_one}/{resource_one}' to='{irc_server_one}' type='chat'><body>QUIT bye bye</body></message>"),
partial(expect_stanza, ("/presence[@from='#foo%{irc_server_one}/{nick_one}'][@type='unavailable']/muc_user:x/muc_user:status[@code='110']",
"/presence[@from='#foo%{irc_server_one}/{nick_one}'][@type='unavailable']/muc_user:x/muc_user:status[@code='110']",)),
- 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("multiple_channels_join",
[
@@ -647,8 +677,6 @@ if __name__ == '__main__':
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("not_connected_error",
[
@@ -693,8 +721,6 @@ if __name__ == '__main__':
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",
[
@@ -824,7 +850,7 @@ if __name__ == '__main__':
handshake_sequence(),
partial(send_stanza,
"<presence from='{jid_one}/{resource_one}' to='#zgeg@{biboumi_host}/{nick_one}' />"),
- connection_sequence("irc.localhost", '{jid_one}/{resource_one}'),
+ connection_sequence("irc.localhost", '{jid_one}/{resource_one}', fixed_irc_server=True),
partial(expect_stanza,
"/message/body[text()='Mode #zgeg [+nt] by {irc_host_one}']"),
partial(expect_stanza,
@@ -883,7 +909,13 @@ if __name__ == '__main__':
partial(expect_stanza, ("/iq[@type='result']/disco_items:query[@node='http://jabber.org/protocol/commands']",
"/iq/disco_items:query/disco_items:item[6]")),
], conf='fixed_server'),
-
+ Scenario("list_muc_user_adhoc",
+ [
+ handshake_sequence(),
+ partial(send_stanza, "<iq type='get' id='idwhatever' from='{jid_admin}/{resource_one}' to='#foo%{irc_server_one}/{nick_one}'><query xmlns='http://jabber.org/protocol/disco#items' node='http://jabber.org/protocol/commands' /></iq>"),
+ partial(expect_stanza, "/iq[@type='error']/error[@type='cancel']/stanza:feature-not-implemented"),
+ ]
+ ),
Scenario("execute_hello_adhoc_command",
[
handshake_sequence(),
@@ -999,7 +1031,6 @@ if __name__ == '__main__':
partial(send_stanza, "<iq type='set' id='command2' from='{jid_admin}/{resource_one}' to='{biboumi_host}'><command xmlns='http://jabber.org/protocol/commands' node='disconnect-from-irc-server' sessionid='{sessionid}' action='next'><x xmlns='jabber:x:data' type='submit'><field var='irc-servers'><value>localhost</value></field><field var='quit-message'><value>Disconnected by e2e</value></field></x></command></iq>"),
partial(expect_unordered, [("/presence[@type='unavailable'][@to='{jid_one}/{resource_one}'][@from='#bon%{irc_server_two}/{nick_three}']",),
("/iq[@type='result']/commands:command[@node='disconnect-from-irc-server'][@status='completed']/commands:note[@type='info'][text()='{jid_one} was disconnected from 1 IRC server.']",),
- ("/message[@to='{jid_one}/{resource_one}']/body[text()='ERROR: Disconnected by e2e']",),
]),
@@ -1017,7 +1048,6 @@ if __name__ == '__main__':
partial(send_stanza, "<iq type='set' id='command2' from='{jid_one}/{resource_one}' to='{biboumi_host}'><command xmlns='http://jabber.org/protocol/commands' node='disconnect-from-irc-server' sessionid='{sessionid}' action='next'><x xmlns='jabber:x:data' type='submit'><field var='irc-servers'><value>irc.localhost</value></field><field var='quit-message'><value>Disconnected by e2e</value></field></x></command></iq>"),
partial(expect_unordered, [("/presence[@type='unavailable'][@to='{jid_one}/{resource_one}'][@from='#foo%{irc_server_one}/{nick_two}']",),
("/iq[@type='result']/commands:command[@node='disconnect-from-irc-server'][@status='completed']/commands:note[@type='info'][text()='{jid_one}/{resource_one} was disconnected from 1 IRC server.']",),
- ("/message[@to='{jid_one}/{resource_one}']/body[text()='ERROR: Disconnected by e2e']",),
]),
]),
Scenario("multisessionnick",
@@ -1065,7 +1095,8 @@ if __name__ == '__main__':
# Message is received with a server-wide JID, by the two resources behind nick_one
partial(expect_stanza, ("/message[@from='{lower_nick_two}%{irc_server_one}'][@to='{jid_one}/{resource_one}'][@type='chat']/body[text()='RELLO']",
"/message/hints:no-copy",
- "/message/carbon:private")),
+ "/message/carbon:private",
+ "!/message/muc_user:x")),
partial(expect_stanza, "/message[@from='{lower_nick_two}%{irc_server_one}'][@to='{jid_one}/{resource_two}'][@type='chat']/body[text()='RELLO']"),
@@ -1172,8 +1203,6 @@ if __name__ == '__main__':
# Second user leaves the channel
partial(send_stanza, "<presence type='unavailable' from='{jid_two}/{resource_one}' to='#foo%{irc_server_one}/{nick_two}' />"),
partial(expect_stanza, "/presence[@type='unavailable'][@from='#foo%{irc_server_one}/{nick_two}']"),
- 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_different_nick",
[
@@ -1259,7 +1288,8 @@ if __name__ == '__main__':
# Respond to the message, to the server-wide JID
partial(send_stanza, "<message from='{jid_two}/{resource_one}' to='{lower_nick_one}%{irc_server_one}' type='chat'><body>yes</body></message>"),
# The response is received from the in-room JID
- partial(expect_stanza, "/message[@from='#foo%{irc_server_one}/{nick_two}'][@to='{jid_one}/{resource_one}'][@type='chat']/body[text()='yes']"),
+ partial(expect_stanza, ("/message[@from='#foo%{irc_server_one}/{nick_two}'][@to='{jid_one}/{resource_one}'][@type='chat']/body[text()='yes']",
+ "/message/muc_user:x")),
## Do the exact same thing, from a different chan,
# to check if the response comes from the right JID
@@ -1437,7 +1467,7 @@ if __name__ == '__main__':
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}'),
+ connection_sequence("irc.localhost", '{jid_one}/{resource_one}', fixed_irc_server=True),
partial(expect_stanza,
"/message/body[text()='Mode #foo [+nt] by {irc_host_one}']"),
partial(expect_stanza,
@@ -1480,6 +1510,24 @@ if __name__ == '__main__':
("/message/subject",),
]),
+ # demonstrate bug https://lab.louiz.org/louiz/biboumi/issues/3291
+ # First user joins an other channel
+ partial(send_stanza,
+ "<presence from='{jid_one}/{resource_one}' to='#bar%{irc_server_one}/{nick_one}' />"),
+ partial(expect_stanza, "/message"),
+ partial(expect_stanza, "/presence/muc_user:x/muc_user:status[@code='110']"),
+ partial(expect_stanza, "/message[@type='groupchat']/subject"),
+
+ # Second user joins
+ partial(send_stanza,
+ "<presence from='{jid_two}/{resource_one}' to='#bar%{irc_server_one}/{nick_two}' />"),
+ partial(expect_unordered, [
+ ("/presence/muc_user:x/muc_user:item[@affiliation='none'][@role='participant']",),
+ ("/presence/muc_user:x/muc_user:item[@affiliation='admin'][@role='moderator']",),
+ ("/presence/muc_user:x/muc_user:status[@code='110']",),
+ ("/message/subject",),
+ ]),
+
# Moderator kicks participant
partial(send_stanza,
"<iq id='kick1' to='#foo%{irc_server_one}' from='{jid_one}/{resource_one}' type='set'><query xmlns='http://jabber.org/protocol/muc#admin'><item nick='{nick_two}' role='none'><reason>reported</reason></item></query></iq>"),
@@ -1495,6 +1543,14 @@ if __name__ == '__main__':
),
("/iq[@id='kick1'][@type='result']",),
]),
+
+ # Bug 3291, suite. We must not receive any presence from #foo, here
+ partial(send_stanza, "<message from='{jid_two}/{resource_one}' to='{irc_server_one}' type='chat'><body>QUIT bye bye</body></message>"),
+ partial(expect_unordered,
+ [("/presence[@from='#bar%{irc_server_one}/{nick_two}'][@to='{jid_one}/{resource_one}']",),
+ ("/presence[@from='#bar%{irc_server_one}/{nick_two}'][@to='{jid_two}/{resource_one}']",),
+ ("/message",),
+ ("/message",)])
]),
Scenario("mode_change",
[
@@ -1803,7 +1859,10 @@ if __name__ == '__main__':
),
partial(expect_stanza,
- "/iq[@type='result'][@id='id1'][@from='#foo%{irc_server_one}'][@to='{jid_one}/{resource_one}']"),
+ ("/iq[@type='result'][@id='id1'][@from='#foo%{irc_server_one}'][@to='{jid_one}/{resource_one}']",
+ "/iq/mam:fin/rms:set/rsm:last",
+ "/iq/mam:fin/rsm:set/rsm:first",
+ "/iq/mam:fin[@complete='true']")),
# Retrieve an empty archive by specifying an early “end” date
partial(send_stanza, """<iq to='#foo%{irc_server_one}' from='{jid_one}/{resource_one}' type='set' id='id2'>
@@ -1815,7 +1874,8 @@ if __name__ == '__main__':
</query></iq>"""),
partial(expect_stanza,
- "/iq[@type='result'][@id='id2'][@from='#foo%{irc_server_one}'][@to='{jid_one}/{resource_one}']"),
+ ("/iq[@type='result'][@id='id2'][@from='#foo%{irc_server_one}'][@to='{jid_one}/{resource_one}']",
+ "/iq/mam:fin[@complete='true']/rsm:set",)),
# Retrieve an empty archive by specifying a late “start” date
# (note that this test will break in ~1000 years)
@@ -1828,7 +1888,8 @@ if __name__ == '__main__':
</query></iq>"""),
partial(expect_stanza,
- "/iq[@type='result'][@id='id3'][@from='#foo%{irc_server_one}'][@to='{jid_one}/{resource_one}']"),
+ ("/iq[@type='result'][@id='id3'][@from='#foo%{irc_server_one}'][@to='{jid_one}/{resource_one}']",
+ "/iq/mam:fin[@complete='true']/rsm:set")),
# Retrieve a limited archive
partial(send_stanza, "<iq to='#foo%{irc_server_one}' from='{jid_one}/{resource_one}' type='set' id='id4'><query xmlns='urn:xmpp:mam:2' queryid='qid4'><set xmlns='http://jabber.org/protocol/rsm'><max>1</max></set></query></iq>"),
@@ -1839,7 +1900,8 @@ if __name__ == '__main__':
),
partial(expect_stanza,
- "/iq[@type='result'][@id='id4'][@from='#foo%{irc_server_one}'][@to='{jid_one}/{resource_one}']"),
+ ("/iq[@type='result'][@id='id4'][@from='#foo%{irc_server_one}'][@to='{jid_one}/{resource_one}']",
+ "/iq/mam:fin[@complete='true']/rsm:set")),
]),
Scenario("mam_with_timestamps",
@@ -1899,15 +1961,134 @@ if __name__ == '__main__':
),
partial(expect_stanza,
- "/iq[@type='result'][@id='id8'][@from='#foo%{irc_server_one}'][@to='{jid_one}/{resource_one}']"),
+ ("/iq[@type='result'][@id='id8'][@from='#foo%{irc_server_one}'][@to='{jid_one}/{resource_one}']",
+ "/iq/mam:fin[@complete='true']/rsm:set")),
+ ]),
+
+
+ Scenario("join_history_limits",
+ [
+ handshake_sequence(),
+ partial(send_stanza,
+ "<presence from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}/{nick_one}' />"),
+ connection_sequence("irc.localhost", '{jid_one}/{resource_one}'),
+ partial(expect_stanza,
+ "/message/body[text()='Mode #foo [+nt] by {irc_host_one}']"),
+ partial(expect_stanza,
+ ("/presence[@to='{jid_one}/{resource_one}'][@from='#foo%{irc_server_one}/{nick_one}']/muc_user:x/muc_user:item[@affiliation='admin'][@role='moderator']",
+ "/presence/muc_user:x/muc_user:status[@code='110']")
+ ),
+ partial(expect_stanza, "/message[@from='#foo%{irc_server_one}'][@type='groupchat']/subject[not(text())]"),
+
+ # Send two channel messages
+ partial(send_stanza, "<message from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}' type='groupchat'><body>coucou</body></message>"),
+ partial(expect_stanza,
+ ("/message[@from='#foo%{irc_server_one}/{nick_one}'][@to='{jid_one}/{resource_one}'][@type='groupchat']/body[text()='coucou']",
+ "/message/stable_id:stanza-id[@by='#foo%{irc_server_one}'][@id]",)
+ ),
+
+ partial(send_stanza, "<message from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}' type='groupchat'><body>coucou 2</body></message>"),
+ # Record the current time
+ partial(expect_stanza, "/message[@from='#foo%{irc_server_one}/{nick_one}'][@to='{jid_one}/{resource_one}'][@type='groupchat']/body[text()='coucou 2']",
+ after = partial(save_current_timestamp_plus_delta, "first_timestamp", datetime.timedelta(seconds=1))),
+
+ # Wait two seconds before sending two new messages
+ partial(sleep_for, 2),
+ partial(send_stanza, "<message from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}' type='groupchat'><body>coucou 3</body></message>"),
+ partial(send_stanza, "<message from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}' type='groupchat'><body>coucou 4</body></message>"),
+ partial(expect_stanza, "/message[@type='groupchat']/body[text()='coucou 3']"),
+ partial(expect_stanza, "/message[@type='groupchat']/body[text()='coucou 4']",
+ after = partial(save_current_timestamp_plus_delta, "second_timestamp", datetime.timedelta(seconds=1))),
+
+ # join the virtual channel, to stay connected to the server even after leaving #foo
+ partial(send_stanza,
+ "<presence from='{jid_one}/{resource_one}' to='%{irc_server_one}/{nick_one}' />"),
+ partial(expect_stanza, "/presence/muc_user:x/muc_user:status[@code='110']"),
+ partial(expect_stanza, "/message/subject"),
+
+ # Leave #foo
+ partial(send_stanza, "<presence from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}' type='unavailable' />"),
+ partial(expect_stanza, "/presence[@type='unavailable']"),
+
+ # Rejoin #foo, with some history limit
+ partial(send_stanza,
+ "<presence from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}/{nick_one}'><x xmlns='http://jabber.org/protocol/muc'><history maxchars='0'/></x></presence>"),
+ partial(expect_stanza, "/message"),
+ partial(expect_stanza, "/presence/muc_user:x/muc_user:status[@code='110']"),
+ partial(expect_stanza, "/message/subject"),
+
+ partial(send_stanza, "<presence from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}' type='unavailable' />"),
+ partial(expect_stanza, "/presence[@type='unavailable']"),
+
+ # Rejoin #foo, with some history limit
+ partial(send_stanza,
+ "<presence from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}/{nick_one}'><x xmlns='http://jabber.org/protocol/muc'><history maxstanzas='3'/></x></presence>"),
+ partial(expect_stanza, "/message"),
+ partial(expect_stanza, "/presence/muc_user:x/muc_user:status[@code='110']"),
+ partial(expect_stanza,
+ "/message[@from='#foo%{irc_server_one}/{nick_one}'][@type='groupchat']/body[text()='coucou 2']"),
+ partial(expect_stanza,
+ "/message[@from='#foo%{irc_server_one}/{nick_one}'][@type='groupchat']/body[text()='coucou 3']"),
+ partial(expect_stanza,
+ "/message[@from='#foo%{irc_server_one}/{nick_one}'][@type='groupchat']/body[text()='coucou 4']"),
+ partial(expect_stanza, "/message/subject"),
+
+ partial(send_stanza, "<presence from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}' type='unavailable' />"),
+ partial(expect_stanza, "/presence[@type='unavailable']"),
+
+
+ # Rejoin #foo, with some history limit
+ partial(send_stanza,
+ "<presence from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}/{nick_one}'><x xmlns='http://jabber.org/protocol/muc'><history since='{first_timestamp}'/></x></presence>"),
+ partial(expect_stanza, "/message"),
+ partial(expect_stanza, "/presence/muc_user:x/muc_user:status[@code='110']"),
+ partial(expect_stanza,
+ "/message[@from='#foo%{irc_server_one}/{nick_one}'][@type='groupchat']/body[text()='coucou 3']"),
+ partial(expect_stanza,
+ "/message[@from='#foo%{irc_server_one}/{nick_one}'][@type='groupchat']/body[text()='coucou 4']"),
+ partial(expect_stanza, "/message/subject"),
+ partial(send_stanza, "<presence from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}' type='unavailable' />"),
+ partial(expect_stanza, "/presence[@type='unavailable']"),
+
+ # Rejoin #foo, with some history limit
+ partial(send_stanza,
+ "<presence from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}/{nick_one}'><x xmlns='http://jabber.org/protocol/muc'><history seconds='1'/></x></presence>"),
+ partial(expect_stanza, "/message"),
+ partial(expect_stanza, "/presence/muc_user:x/muc_user:status[@code='110']"),
+ partial(expect_stanza,
+ "/message[@from='#foo%{irc_server_one}/{nick_one}'][@type='groupchat']/body[text()='coucou 3']"),
+ partial(expect_stanza,
+ "/message[@from='#foo%{irc_server_one}/{nick_one}'][@type='groupchat']/body[text()='coucou 4']"),
+ partial(expect_stanza, "/message/subject"),
+ partial(send_stanza, "<presence from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}' type='unavailable' />"),
+ partial(expect_stanza, "/presence[@type='unavailable']"),
+
+ # Rejoin #foo, with some history limit
+ partial(send_stanza,
+ "<presence from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}/{nick_one}'><x xmlns='http://jabber.org/protocol/muc'><history seconds='5'/></x></presence>"),
+ partial(expect_stanza, "/message"),
+ partial(expect_stanza, "/presence/muc_user:x/muc_user:status[@code='110']"),
+ partial(expect_stanza,
+ "/message[@from='#foo%{irc_server_one}/{nick_one}'][@type='groupchat']/body[text()='coucou']"), partial(expect_stanza,
+ "/message[@from='#foo%{irc_server_one}/{nick_one}'][@type='groupchat']/body[text()='coucou 2']"),
+ partial(expect_stanza,
+ "/message[@from='#foo%{irc_server_one}/{nick_one}'][@type='groupchat']/body[text()='coucou 3']"),
+ partial(expect_stanza,
+ "/message[@from='#foo%{irc_server_one}/{nick_one}'][@type='groupchat']/body[text()='coucou 4']"),
+ partial(expect_stanza, "/message/subject"),
+ partial(send_stanza, "<presence from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}' type='unavailable' />"),
+ partial(expect_stanza, "/presence[@type='unavailable']"),
+
]),
+
+
Scenario("mam_on_fixed_server",
[
handshake_sequence(),
partial(send_stanza,
"<presence from='{jid_one}/{resource_one}' to='#foo@{biboumi_host}/{nick_one}' />"),
- connection_sequence("irc.localhost", '{jid_one}/{resource_one}'),
+ connection_sequence("irc.localhost", '{jid_one}/{resource_one}', fixed_irc_server=True),
partial(expect_stanza,
"/message/body[text()='Mode #foo [+nt] by {irc_host_one}']"),
partial(expect_stanza,
@@ -1975,8 +2156,11 @@ if __name__ == '__main__':
("/message/mam:result[@queryid='qid1']/forward:forwarded/delay:delay",
"/message/mam:result[@queryid='qid1']/forward:forwarded/client:message[@from='#foo%{irc_server_one}/{nick_one}'][@type='groupchat']/client:body[text()='149']")
),
+ # And it should not be marked as complete
partial(expect_stanza,
- "/iq[@type='result'][@id='id1'][@from='#foo%{irc_server_one}'][@to='{jid_one}/{resource_one}']"),
+ ("/iq[@type='result'][@id='id1'][@from='#foo%{irc_server_one}'][@to='{jid_one}/{resource_one}']",
+ "!/iq//mam:fin[@complete='true']",
+ "/iq//mam:fin")),
]),
Scenario("channel_history_on_fixed_server",
@@ -1985,7 +2169,7 @@ if __name__ == '__main__':
# 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}'),
+ connection_sequence("irc.localhost", '{jid_one}/{resource_one}', fixed_irc_server=True),
partial(expect_stanza,
"/message/body[text()='Mode #foo [+nt] by {irc_host_one}']"),
partial(expect_stanza,
@@ -2078,6 +2262,21 @@ if __name__ == '__main__':
"/iq/disco_items:query/disco_items:item[@jid='#bar%{irc_server_one}']"
))
]),
+ Scenario("channel_list_escaping",
+ [
+ handshake_sequence(),
+
+ partial(send_stanza,
+ "<presence from='{jid_one}/{resource_one}' to='#true\\2ffalse%{irc_server_one}/{nick_one}' />"),
+ connection_sequence("irc.localhost", '{jid_one}/{resource_one}'),
+ partial(expect_stanza,
+ "/message/body[text()='Mode #true/false [+nt] by {irc_host_one}']"),
+ partial(expect_stanza,
+ ("/presence[@to='{jid_one}/{resource_one}'][@from='#true\\2ffalse%{irc_server_one}/{nick_one}']/muc_user:x/muc_user:item[@affiliation='admin'][@role='moderator']",
+ "/presence/muc_user:x/muc_user:status[@code='110']")
+ ),
+ partial(expect_stanza, "/message[@from='#true\\2ffalse%{irc_server_one}'][@type='groupchat']/subject[not(text())]"),
+ ]),
Scenario("channel_list_with_rsm",
[
handshake_sequence(),
@@ -2376,6 +2575,18 @@ if __name__ == '__main__':
partial(send_stanza, "<message from='{jid_one}/{resource_one}' to='{irc_server_one}' type='chat'><body>WHOIS {nick_one}</body></message>"),
partial(expect_stanza, "/message[@from='{irc_server_one}'][@type='chat']/body[text()='irc.localhost: {nick_one} ~{nick_one} localhost * {nick_one}']"),
]),
+ Scenario("raw_message_fixed_irc_server",
+ [
+ handshake_sequence(),
+ partial(send_stanza, "<presence from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}/{nick_one}' />"),
+ connection_sequence("irc.localhost", '{jid_one}/{resource_one}', fixed_irc_server=True),
+ partial(expect_stanza, "/message"),
+ partial(expect_stanza, "/presence"),
+ partial(expect_stanza, "/message"),
+
+ partial(send_stanza, "<message from='{jid_one}/{resource_one}' to='{biboumi_host}' type='chat'><body>WHOIS {nick_one}</body></message>"),
+ partial(expect_stanza, "/message[@from='{biboumi_host}'][@type='chat']/body[text()='irc.localhost: {nick_one} ~{nick_one} localhost * {nick_one}']"),
+ ], conf='fixed_server'),
Scenario("self_disco_info",
[
handshake_sequence(),
@@ -2459,8 +2670,6 @@ if __name__ == '__main__':
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",
[
@@ -2471,6 +2680,7 @@ if __name__ == '__main__':
"/iq/commands:command/dataform:x[@type='form']/dataform:instructions[text()='Edit the form, to configure your global settings for the component.']",
"/iq/commands:command/dataform:x[@type='form']/dataform:field[@type='text-single'][@var='max_history_length']/dataform:value[text()='20']",
"/iq/commands:command/dataform:x[@type='form']/dataform:field[@type='boolean'][@var='record_history']/dataform:value[text()='true']",
+ "/iq/commands:command/dataform:x[@type='form']/dataform:field[@type='boolean'][@var='persistent']/dataform:value[text()='false']",
"/iq/commands:command/commands:actions/commands:next",
),
after = partial(save_value, "sessionid", partial(extract_attribute, "/iq[@type='result']/commands:command[@node='configure']", "sessionid"))
@@ -2492,6 +2702,20 @@ if __name__ == '__main__':
partial(send_stanza, "<iq type='set' id='id4' from='{jid_one}/{resource_one}' to='{biboumi_host}'><command xmlns='http://jabber.org/protocol/commands' action='cancel' node='configure' sessionid='{sessionid}' /></iq>"),
partial(expect_stanza, "/iq[@type='result']/commands:command[@node='configure'][@status='canceled']"),
]),
+ Scenario("global_configure_persistent_by_default",
+ [
+ handshake_sequence(),
+ partial(send_stanza, "<iq type='set' id='id1' from='{jid_one}/{resource_one}' to='{biboumi_host}'><command xmlns='http://jabber.org/protocol/commands' node='configure' action='execute' /></iq>"),
+ partial(expect_stanza, ("/iq[@type='result']/commands:command[@node='configure'][@sessionid][@status='executing']",
+ "/iq/commands:command/dataform:x[@type='form']/dataform:title[text()='Configure some global default settings.']",
+ "/iq/commands:command/dataform:x[@type='form']/dataform:instructions[text()='Edit the form, to configure your global settings for the component.']",
+ "/iq/commands:command/dataform:x[@type='form']/dataform:field[@type='text-single'][@var='max_history_length']/dataform:value[text()='20']",
+ "/iq/commands:command/dataform:x[@type='form']/dataform:field[@type='boolean'][@var='record_history']/dataform:value[text()='true']",
+ "/iq/commands:command/dataform:x[@type='form']/dataform:field[@type='boolean'][@var='persistent']/dataform:value[text()='true']",
+ "/iq/commands:command/commands:actions/commands:next",
+ ),
+ ),
+ ],conf='persistent_by_default'),
Scenario("irc_server_configure",
[
handshake_sequence(),
@@ -2726,7 +2950,7 @@ if __name__ == '__main__':
partial(send_stanza,
"<presence from='{jid_one}/{resource_one}' to='#foo@{biboumi_host}/{nick_one}' />"),
- connection_sequence("irc.localhost", '{jid_one}/{resource_one}'),
+ connection_sequence("irc.localhost", '{jid_one}/{resource_one}', fixed_irc_server=True),
partial(expect_stanza, "/message"),
partial(expect_stanza, "/presence"),
partial(expect_stanza, "/message"),
@@ -2785,6 +3009,60 @@ if __name__ == '__main__':
partial(send_stanza, "<presence from='{jid_one}' to='{biboumi_host}' type='unavailable' />"),
partial(send_stanza, "<presence from='{jid_one}' to='{biboumi_host}' type='unsubscribed' />"),
]),
+ Scenario("resource_is_removed_from_server_when_last_chan_is_left",
+ [
+ # Join the channel
+ handshake_sequence(),
+ partial(send_stanza,
+ "<presence from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}/{nick_one}' />"),
+ connection_sequence("irc.localhost", '{jid_one}/{resource_one}'),
+ partial(expect_stanza,
+ "/message/body[text()='Mode #foo [+nt] by {irc_host_one}']"),
+ partial(expect_stanza,
+ ("/presence[@to='{jid_one}/{resource_one}'][@from='#foo%{irc_server_one}/{nick_one}']/muc_user:x/muc_user:item[@affiliation='admin'][@role='moderator']",
+ "/presence/muc_user:x/muc_user:status[@code='110']")
+ ),
+ partial(expect_stanza, "/message[@from='#foo%{irc_server_one}'][@type='groupchat'][@to='{jid_one}/{resource_one}']/subject[not(text())]"),
+
+ # Make it persistent
+ partial(send_stanza, "<iq from='{jid_one}/{resource_one}' id='conf1' to='#foo%{irc_server_one}' type='get'><query xmlns='http://jabber.org/protocol/muc#owner'/></iq>"),
+ partial(expect_stanza, "/iq[@type='result']/muc_owner:query/dataform:x/dataform:field[@var='persistent'][@type='boolean']/dataform:value[text()='false']"),
+ partial(send_stanza, "<iq from='{jid_one}/{resource_one}' id='conf2' to='#foo%{irc_server_one}' type='set'><query xmlns='http://jabber.org/protocol/muc#owner'><x type='submit' xmlns='jabber:x:data'><field var='persistent' xmlns='jabber:x:data'><value>true</value></field></x></query></iq>"),
+ partial(expect_stanza, "/iq[@type='result']"),
+
+ partial(send_stanza, "<presence from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}/{nick_one}' type='unavailable' />"),
+ partial(expect_stanza, "/presence[@type='unavailable'][@from='#foo%{irc_server_one}/{nick_one}']"),
+
+ # Join the same channel, with the same JID, but a different resource
+ partial(send_stanza,
+ "<presence from='{jid_one}/{resource_two}' to='#foo%{irc_server_one}/{nick_one}' />"),
+ partial(expect_stanza,
+ ("/presence[@to='{jid_one}/{resource_two}'][@from='#foo%{irc_server_one}/{nick_one}']/muc_user:x/muc_user:item[@affiliation='admin'][@role='moderator']",
+ "/presence/muc_user:x/muc_user:status[@code='110']")
+ ),
+ partial(expect_stanza, "/message[@from='#foo%{irc_server_one}'][@type='groupchat'][@to='{jid_one}/{resource_two}']/subject[not(text())]"),
+
+ # Join some other channel with someone else
+ partial(send_stanza,
+ "<presence from='{jid_two}/{resource_one}' to='#bar%{irc_server_one}/{nick_two}' />"),
+ connection_sequence("irc.localhost", '{jid_two}/{resource_one}'),
+ partial(expect_stanza,
+ "/message/body[text()='Mode #bar [+nt] by {irc_host_one}']"),
+ partial(expect_stanza,
+ ("/presence[@to='{jid_two}/{resource_one}'][@from='#bar%{irc_server_one}/{nick_two}']/muc_user:x/muc_user:item[@affiliation='admin'][@role='moderator']",
+ "/presence/muc_user:x/muc_user:status[@code='110']")
+ ),
+ partial(expect_stanza, "/message[@from='#bar%{irc_server_one}'][@type='groupchat'][@to='{jid_two}/{resource_one}']/subject[not(text())]"),
+
+ # Send two messages from the second user to the first one
+ partial(send_stanza, "<message from='{jid_two}/{resource_one}' to='{lower_nick_one}%{irc_server_one}' type='chat'><body>kikoo</body></message>"),
+ partial(send_stanza, "<message from='{jid_two}/{resource_one}' to='{lower_nick_one}%{irc_server_one}' type='chat'><body>second kikoo</body></message>"),
+
+ # We must receive each message only once, no duplicate
+ partial(expect_stanza, "/message/body[text()='kikoo']"),
+ partial(expect_stanza, "/message/body[text()='second kikoo']"),
+ ]
+ ),
Scenario("irc_server_presence_in_roster",
[
handshake_sequence(),
@@ -2801,7 +3079,7 @@ if __name__ == '__main__':
"<presence from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}/{nick_one}' />"),
# We must receive the IRC server presence, in the connection sequence
- connection_sequence("irc.localhost", '{jid_one}/{resource_one}', True),
+ connection_sequence("irc.localhost", '{jid_one}/{resource_one}', expected_irc_presence=True),
partial(expect_stanza,
"/message/body[text()='Mode #foo [+nt] by {irc_host_one}']"),
partial(expect_stanza,
@@ -2813,8 +3091,6 @@ if __name__ == '__main__':
# Leave the channel, and thus the IRC server
partial(send_stanza, "<presence type='unavailable' from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}/{nick_one}' />"),
partial(expect_stanza, "/presence[@type='unavailable'][@from='#foo%{irc_server_one}/{nick_one}']"),
- partial(expect_stanza, "/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.']"),
partial(expect_stanza, "/presence[@from='{irc_server_one}'][@to='{jid_one}'][@type='unavailable']"),
])
)
diff --git a/tests/logger.cpp b/tests/logger.cpp
index 1d59a22..1e3392a 100644
--- a/tests/logger.cpp
+++ b/tests/logger.cpp
@@ -10,13 +10,8 @@ using namespace std::string_literals;
TEST_CASE("Basic logging")
{
-#ifdef SYSTEMD_FOUND
- const std::string debug_header = "<7>";
- const std::string error_header = "<3>";
-#else
const std::string debug_header = "[DEBUG]: ";
const std::string error_header = "[ERROR]: ";
-#endif
Logger::instance().reset();
GIVEN("A logger with log_level 0")
{
diff --git a/tests/utils.cpp b/tests/utils.cpp
index c5ef7e7..6de19f0 100644
--- a/tests/utils.cpp
+++ b/tests/utils.cpp
@@ -11,6 +11,7 @@
#include <utils/system.hpp>
#include <utils/scopeguard.hpp>
#include <utils/dirname.hpp>
+#include <utils/is_one_of.hpp>
using namespace std::string_literals;
@@ -171,3 +172,14 @@ TEST_CASE("dirname")
CHECK(utils::dirname(".") == ".");
CHECK(utils::dirname("./") == "./");
}
+
+TEST_CASE("is_in")
+{
+ CHECK((is_one_of<int, float, std::string, int>) == true);
+ CHECK((is_one_of<int, float, std::string>) == false);
+ CHECK((is_one_of<int>) == false);
+ CHECK((is_one_of<int, int>) == true);
+ CHECK((is_one_of<bool, int>) == false);
+ CHECK((is_one_of<bool, bool>) == true);
+ CHECK((is_one_of<bool, bool, bool, bool, bool, int>) == true);
+}