aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.github/workflows/ci.yml143
-rw-r--r--.shellcheckrc4
-rw-r--r--CHANGELOG.md47
-rw-r--r--Makefile.in32
-rw-r--r--ejabberd.service.template4
-rwxr-xr-xejabberdctl.template21
-rw-r--r--include/mod_muc_room.hrl2
-rw-r--r--include/pubsub.hrl2
-rw-r--r--man/ejabberd.yml.5339
-rw-r--r--mix.exs17
-rw-r--r--mix.lock22
-rw-r--r--priv/msgs/fr.msg50
-rw-r--r--priv/msgs/zh.msg33
-rw-r--r--rebar.config32
-rw-r--r--rebar.config.script4
-rw-r--r--rel/reltool.config.script1
-rw-r--r--sql/lite.new.sql1
-rw-r--r--sql/lite.sql1
-rw-r--r--sql/mssql.sql1
-rw-r--r--sql/mysql.new.sql1
-rw-r--r--sql/mysql.old-to-new.sql2
-rw-r--r--sql/mysql.sql1
-rw-r--r--sql/pg.new.sql3
-rw-r--r--sql/pg.sql1
-rw-r--r--src/ejabberd_admin.erl36
-rw-r--r--src/ejabberd_auth_sql.erl14
-rw-r--r--src/ejabberd_commands.erl1
-rw-r--r--src/ejabberd_ctl.erl7
-rw-r--r--src/ejabberd_piefxis.erl126
-rw-r--r--src/ejabberd_s2s.erl79
-rw-r--r--src/ejabberd_s2s_out.erl22
-rw-r--r--src/ejabberd_sql_pt.erl77
-rw-r--r--src/ejd2sql.erl15
-rw-r--r--src/gen_pubsub_node.erl4
-rw-r--r--src/mod_admin_update_sql.erl2
-rw-r--r--src/mod_caps.erl14
-rw-r--r--src/mod_conversejs.erl157
-rw-r--r--src/mod_conversejs_opt.erl41
-rw-r--r--src/mod_http_api.erl1
-rw-r--r--src/mod_mam.erl1
-rw-r--r--src/mod_mqtt_session.erl4
-rw-r--r--src/mod_muc.erl39
-rw-r--r--src/mod_muc_admin.erl67
-rw-r--r--src/mod_muc_admin_opt.erl13
-rw-r--r--src/mod_muc_opt.erl7
-rw-r--r--src/mod_muc_room.erl472
-rw-r--r--src/mod_muc_sql.erl9
-rw-r--r--src/mod_multicast.erl426
-rw-r--r--src/mod_pubsub.erl99
-rw-r--r--src/mod_pubsub_opt.erl9
-rw-r--r--src/mod_push_sql.erl2
-rw-r--r--src/mod_register.erl27
-rw-r--r--src/mod_register_opt.erl7
-rw-r--r--src/mod_register_web.erl34
-rw-r--r--src/mod_roster_sql.erl10
-rw-r--r--src/mod_shared_roster.erl7
-rw-r--r--src/mod_shared_roster_ldap.erl10
-rw-r--r--src/mod_stun_disco.erl2
-rw-r--r--src/node_flat.erl18
-rw-r--r--src/node_flat_sql.erl19
-rw-r--r--src/node_pep.erl7
-rw-r--r--src/node_pep_sql.erl5
-rw-r--r--src/prosody2ejabberd.erl2
-rw-r--r--src/rest.erl15
-rw-r--r--test/roster_tests.erl10
-rwxr-xr-xtools/captcha.sh69
-rw-r--r--vars.config.in5
67 files changed, 2027 insertions, 728 deletions
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index d498760be..8cd9a38c8 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -25,12 +25,12 @@ jobs:
strategy:
fail-fast: false
matrix:
- otp: ['19.3', '24.0']
+ otp: ['19.3', '24']
include:
- otp: '19.3'
rebar: 2
os: ubuntu-18.04
- - otp: '24.0'
+ - otp: '24'
rebar: 3
os: ubuntu-20.04
runs-on: ${{ matrix.os }}
@@ -44,9 +44,17 @@ jobs:
- uses: actions/checkout@v2
+ - name: Test shell scripts
+ if: matrix.otp == 24
+ run: |
+ shellcheck test/ejabberd_SUITE_data/gencerts.sh
+ shellcheck tools/captcha.sh
+ shellcheck ejabberd.init.template
+ shellcheck -x ejabberdctl.template
+
- name: Get previous Erlang/OTP
uses: ErlGang/setup-erlang@master
- if: matrix.otp != 24.0
+ if: matrix.otp != 24
with:
otp-version: ${{ matrix.otp }}
@@ -81,7 +89,6 @@ jobs:
libsqlite3-dev libwebp-dev libyaml-dev
- name: Prepare rebar
- id: rebar
run: |
echo '{xref_ignores, [{eldap_filter_yecc, return_error, 2}
]}.' >>rebar.config
@@ -93,23 +100,26 @@ jobs:
mqtree, p1_acme, p1_mysql, p1_oauth2, p1_pgsql, p1_utils, pkix,
sqlite3, stringprep, stun, xmpp, yconf]} ]}.' >>rebar.config
echo '{ct_extra_params, "-verbosity 20"}.' >>rebar.config
+ echo "{ct_opts, [{verbosity, 20}, {keep_logs, 20}]}." >>rebar.config
- - name: Cache rebar2
- if: matrix.rebar == 2
+ - name: Cache rebar
uses: actions/cache@v2
with:
path: |
deps/
dialyzer/
ebin/
+ ~/.cache/rebar3/
key: ${{matrix.otp}}-${{matrix.rebar}}-${{hashFiles('rebar.config')}}
- - name: Cache rebar3
- if: matrix.rebar == 3
- uses: actions/cache@v2
- with:
- path: ~/.cache/rebar3/
- key: ${{matrix.otp}}-${{matrix.rebar}}-${{hashFiles('rebar.config')}}
+ - name: Download test logs
+ if: matrix.otp == 24 && github.repository == 'processone/ejabberd'
+ continue-on-error: true
+ run: |
+ mkdir -p _build/test
+ curl -sSL https://github.com/processone/ecil/tarball/gh-pages |
+ tar -C _build/test --strip-components=1 --wildcards -xzf -
+ rm -rf _build/test/logs/last/
- name: Compile
run: |
@@ -128,46 +138,50 @@ jobs:
- run: make hooks
- run: make options
- run: make xref
- - run: make dialyzer
- - run: make test
+ - run: |
+ make dialyzer
+ [ ${{ matrix.rebar }} = 3 ] && true \
+ || { cat dialyzer/error.log ; test ! -s dialyzer/error.log ; }
+
+ - name: Run tests
+ if: matrix.otp != 24
+ run: make test
+ - name: Run tests (OTP 24)
+ if: matrix.otp == 24
+ id: ct
+ run: |
+ (cd priv && ln -sf ../sql)
+ COMMIT=`echo $GITHUB_SHA | cut -c 1-7`
+ DATE=`date +%s`
+ REF_NAME=`echo $GITHUB_REF_NAME | tr "/" "_"`
+ NODENAME=$DATE@$GITHUB_RUN_NUMBER-$GITHUB_ACTOR-$REF_NAME-$COMMIT
+ LABEL=`git show -s --format=%s | cut -c 1-30`
+ rebar3 ct --name $NODENAME --label "$LABEL"
+ rebar3 cover
- name: Check results
if: always()
run: |
- [[ -d _build ]] && ln -s _build/test/logs/ logs \
- && ln `find _build/ -name "*dialyzer_warnings"` \
- logs/dialyzer.log \
- || ln dialyzer/error.log logs/dialyzer.log
+ [[ -d _build ]] && ln -s _build/test/logs/last/ logs || true
ln `find logs/ -name suite.log` logs/suite.log
grep 'TEST COMPLETE' logs/suite.log
grep -q 'TEST COMPLETE,.* 0 failed' logs/suite.log
test $(find logs/ -empty -name error.log)
- - name: View dialyzer report
- run: cat logs/dialyzer.log
-
- - name: View full suite.log
- run: cat logs/suite.log
-
- - name: View suite.log failures
+ - name: View logs failures
if: failure()
- run: cat logs/suite.log | awk
- 'BEGIN{RS="\n=case";FS="\n"} /=result\s*failed/ {print "=case" $0}'
-
- - name: View full ejabberd.log
- if: failure()
- run: find logs/ -name ejabberd.log -exec cat '{}' ';'
-
- - name: View exunit.log
- if: failure()
- run: find logs/ -name exunit.log -exec cat '{}' ';'
+ run: |
+ cat logs/suite.log | awk \
+ 'BEGIN{RS="\n=case";FS="\n"} /=result\s*failed/ {print "=case" $0}'
+ find logs/ -name error.log -exec cat '{}' ';'
+ find logs/ -name exunit.log -exec cat '{}' ';'
- name: Send to coveralls
- if: matrix.otp == 24.0
+ if: matrix.otp == 24
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
- rebar3 as test coveralls send
+ DIAGNOSTIC=1 rebar3 as test coveralls send
curl -v -k https://coveralls.io/webhook \
--header "Content-Type: application/json" \
--data '{"repo_name":"$GITHUB_REPOSITORY",
@@ -175,6 +189,61 @@ jobs:
"payload":{"build_num":$GITHUB_RUN_ID,
"status":"done"}}'
+ - name: Upload test logs
+ if: always() && steps.ct.outcome == 'failure' && github.repository == 'processone/ejabberd'
+ uses: peaceiris/actions-gh-pages@v3
+ with:
+ publish_dir: _build/test
+ exclude_assets: '.github,lib,plugins'
+ external_repository: processone/ecil
+ deploy_key: ${{ secrets.ACTIONS_DEPLOY_KEY }}
+ keep_files: true
+
+ - name: View ECIL address
+ if: always() && steps.ct.outcome == 'failure' && github.repository == 'processone/ejabberd'
+ run: |
+ CTRUN=`ls -la _build/test/logs/last | sed 's|.*-> ||'`
+ echo "::notice::View CT results: https://processone.github.io/ecil/logs/$CTRUN/"
+
+ - name: Prepare new schema
+ run: |
+ [[ -d logs ]] && rm -rf logs
+ [[ -d _build/test/logs ]] && rm -rf _build/test/logs || true
+ mysql -u root -proot -e "DROP DATABASE ejabberd_test;"
+ sudo -u postgres psql -c "DROP DATABASE ejabberd_test;"
+ mysql -u root -proot -e "CREATE DATABASE ejabberd_test;"
+ mysql -u root -proot -e "GRANT ALL ON ejabberd_test.*
+ TO 'ejabberd_test'@'localhost';"
+ mysql -u root -proot ejabberd_test < sql/mysql.new.sql
+ sudo -u postgres psql -c "CREATE DATABASE ejabberd_test;"
+ sudo -u postgres psql ejabberd_test -f sql/pg.new.sql
+ sudo -u postgres psql -c "GRANT ALL PRIVILEGES
+ ON DATABASE ejabberd_test TO ejabberd_test;"
+ sudo -u postgres psql ejabberd_test -c "GRANT ALL PRIVILEGES ON ALL
+ TABLES IN SCHEMA public
+ TO ejabberd_test;"
+ sudo -u postgres psql ejabberd_test -c "GRANT ALL PRIVILEGES ON ALL
+ SEQUENCES IN SCHEMA public
+ TO ejabberd_test;"
+ sudo sed -i 's|new_schema, false|new_schema, true|g' test/suite.erl
+ - run: CT_BACKENDS=mysql,pgsql make test
+ id: ctnewschema
+ - name: Check results
+ if: always() && steps.ctnewschema.outcome != 'skipped'
+ run: |
+ [[ -d _build ]] && ln -s _build/test/logs/last/ logs || true
+ ln `find logs/ -name suite.log` logs/suite.log
+ grep 'TEST COMPLETE' logs/suite.log
+ grep -q 'TEST COMPLETE,.* 0 failed' logs/suite.log
+ test $(find logs/ -empty -name error.log)
+ - name: View logs failures
+ if: failure() && steps.ctnewschema.outcome != 'skipped'
+ run: |
+ cat logs/suite.log | awk \
+ 'BEGIN{RS="\n=case";FS="\n"} /=result\s*failed/ {print "=case" $0}'
+ find logs/ -name error.log -exec cat '{}' ';'
+ find logs/ -name exunit.log -exec cat '{}' ';'
+
binaries:
name: Binaries
needs: [tests]
diff --git a/.shellcheckrc b/.shellcheckrc
new file mode 100644
index 000000000..0b7131a2e
--- /dev/null
+++ b/.shellcheckrc
@@ -0,0 +1,4 @@
+disable=SC2016,SC2086,SC2089,SC2090
+external-sources=true
+source=ejabberdctl.cfg.example
+shell=sh
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 13385b864..288931b5c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,50 @@
+# Version 21.12
+
+Commands
+- `create_room_with_opts`: Fixed when using SQL storage
+- `change_room_option`: Add missing fields from config inside `mod_muc_admin:change_options`
+- piefxis: Fixed arguments of all commands
+
+Modules
+- mod_caps: Don't forget caps on XEP-0198 resumption
+- mod_conversejs: New module to serve a simple page for Converse.js
+- mod_http_upload_quota: Avoid `max_days` race
+- mod_muc: Support MUC hats (XEP-0317, conversejs/prosody compatible)
+- mod_muc: Optimize MucSub processing
+- mod_muc: Fix exception in mucsub {un}subscription events multicast handler
+- mod_multicast: Improve and optimize multicast routing code
+- mod_offline: Allow storing non-composing x:events in offline
+- mod_ping: Send ping from server, not bare user JID
+- mod_push: Fix handling of MUC/Sub messages
+- mod_register: New allow_modules option to restrict registration modules
+- mod_register_web: Handle unknown host gracefully
+- mod_register_web: Use mod_register configured restrictions
+
+PubSub
+- Add `delete_expired_pubsub_items` command
+- Add `delete_old_pubsub_items` command
+- Optimize publishing on large nodes (SQL)
+- Support unlimited number of items
+- Support `max_items=max` node configuration
+- Bump default value for `max_items` limit from 10 to 1000
+- Use configured `max_items` by default
+- node_flat: Avoid catch-all clauses for RSM
+- node_flat_sql: Avoid catch-all clauses for RSM
+
+SQL
+- Use `INSERT ... ON CONFLICT` in SQL_UPSERT for PostgreSQL >= 9.5
+- mod_mam export: assign MUC entries to the MUC service
+- MySQL: Fix typo when creating index
+- PgSQL: Add SASL auth support, PostgreSQL 14
+- PgSQL: Add missing SQL migration for table `push_session`
+- PgSQL: Fix `vcard_search` definition in pgsql new schema
+
+Other
+- `captcha-ng.sh`: "sort -R" command not POSIX, added "shuf" and "cat" as fallback
+- Make s2s connection table cleanup more robust
+- Update export/import of scram password to XEP-0227 1.1
+- Update Jose to 1.11.1 (the last in hex.pm correctly versioned)
+
# Version 21.07
Compilation
diff --git a/Makefile.in b/Makefile.in
index 6549665f7..3066488f0 100644
--- a/Makefile.in
+++ b/Makefile.in
@@ -68,12 +68,6 @@ LUADIR = $(PRIVDIR)/lua
# /var/lib/ejabberd/
SPOOLDIR = $(DESTDIR)@localstatedir@/lib/ejabberd
-# /var/lock/ejabberdctl
-CTLLOCKDIR = $(DESTDIR)@localstatedir@/lock/ejabberdctl
-
-# /var/lib/ejabberd/.erlang.cookie
-COOKIEFILE = $(SPOOLDIR)/.erlang.cookie
-
# /var/log/ejabberd/
LOGDIR = $(DESTDIR)@localstatedir@/log/ejabberd
@@ -100,8 +94,10 @@ endif
ifeq "$(MIX)" "mix"
REBAR_VER:=6
+REBAR_VER_318:=0
else
REBAR_VER:=$(shell $(REBAR) --version | awk -F '[ .]' '/rebar / {print $$2}')
+REBAR_VER_318:=$(shell $(REBAR) --version | awk -F '[ .]' '/rebar / {print ($$2 == 3 && $$3 >= 18 ? 1 : 0)}')
endif
ifeq "$(REBAR_VER)" "6"
@@ -121,7 +117,11 @@ else
ifeq "$(REBAR_VER)" "3"
SKIPDEPS=
LISTDEPS=tree
+ifeq "$(REBAR_VER_318)" "1"
+ UPDATEDEPS=upgrade --all
+else
UPDATEDEPS=upgrade
+endif
DEPSPATTERN="s/ (.*//; /^ / s/.* \([a-z0-9_]*\).*/\1/p;"
DEPSBASE=_build
DEPSDIR=$(DEPSBASE)/default/lib
@@ -249,7 +249,15 @@ $(call TO_DEST,priv/bin/captcha.sh): tools/captcha.sh $(call TO_DEST,priv/bin)
$(call TO_DEST,priv/lua/redis_sm.lua): priv/lua/redis_sm.lua $(call TO_DEST,priv/lua)
$(INSTALL) -m 644 $< $@
-copy-files-sub2: $(call TO_DEST,$(DEPS_FILES) $(MAIN_FILES) priv/bin/captcha.sh priv/sql/lite.sql priv/sql/lite.new.sql priv/lua/redis_sm.lua)
+ifeq (@sqlite@,true)
+SQLITE_FILES = priv/sql/lite.sql priv/sql/lite.new.sql
+endif
+
+ifeq (@redis@,true)
+REDIS_FILES = priv/lua/redis_sm.lua
+endif
+
+copy-files-sub2: $(call TO_DEST,$(DEPS_FILES) $(MAIN_FILES) priv/bin/captcha.sh $(SQLITE_FILES) $(REDIS_FILES))
.PHONY: $(call TO_DEST,$(DEPS_FILES) $(MAIN_DIRS) $(DEPS_DIRS))
@@ -298,7 +306,8 @@ install: copy-files
chmod 755 ejabberd.init
#
# Service script
- $(SED) -e "s*@ctlscriptpath@*$(SBINDIR)*g" ejabberd.service.template \
+ $(SED) -e "s*@ctlscriptpath@*$(SBINDIR)*g" \
+ -e "s*@installuser@*$(INIT_USER)*g" ejabberd.service.template \
> ejabberd.service
chmod 644 ejabberd.service
#
@@ -306,12 +315,6 @@ install: copy-files
$(INSTALL) -d -m 750 $(O_USER) $(SPOOLDIR)
$(CHOWN_COMMAND) -R @INSTALLUSER@ $(SPOOLDIR) >$(CHOWN_OUTPUT)
chmod -R 750 $(SPOOLDIR)
- [ ! -f $(COOKIEFILE) ] || { $(CHOWN_COMMAND) @INSTALLUSER@ $(COOKIEFILE) >$(CHOWN_OUTPUT) ; chmod 400 $(COOKIEFILE) ; }
- #
- # ejabberdctl lock directory
- $(INSTALL) -d -m 750 $(O_USER) $(CTLLOCKDIR)
- $(CHOWN_COMMAND) -R @INSTALLUSER@ $(CTLLOCKDIR) >$(CHOWN_OUTPUT)
- chmod -R 750 $(CTLLOCKDIR)
#
# Log directory
$(INSTALL) -d -m 750 $(O_USER) $(LOGDIR)
@@ -361,7 +364,6 @@ uninstall-all: uninstall-binary
rm -rf $(ETCDIR)
rm -rf $(EJABBERDDIR)
rm -rf $(SPOOLDIR)
- rm -rf $(CTLLOCKDIR)
rm -rf $(LOGDIR)
clean:
diff --git a/ejabberd.service.template b/ejabberd.service.template
index c779ea031..685a104d0 100644
--- a/ejabberd.service.template
+++ b/ejabberd.service.template
@@ -4,8 +4,8 @@ After=network.target
[Service]
Type=notify
-User=ejabberd
-Group=ejabberd
+User=@installuser@
+Group=@installuser@
LimitNOFILE=65536
Restart=on-failure
RestartSec=5
diff --git a/ejabberdctl.template b/ejabberdctl.template
index 4cab19977..408a4fe6d 100755
--- a/ejabberdctl.template
+++ b/ejabberdctl.template
@@ -12,6 +12,8 @@ ERLANG_NODE=ejabberd@localhost
# define default environment variables
[ -z "$SCRIPT" ] && SCRIPT=$0
SCRIPT_DIR="$(cd "$(dirname "$SCRIPT")" && pwd -P)"
+# shellcheck disable=SC2034
+ERTS_VSN="{{erts_vsn}}"
ERL="{{erl}}"
IEX="{{bindir}}/iex"
EPMD="{{epmd}}"
@@ -62,6 +64,7 @@ done
: "${EJABBERDCTL_CONFIG_PATH:="$ETC_DIR/ejabberdctl.cfg"}"
# Allows passing extra Erlang command-line arguments in vm.args file
: "${VMARGS:="$ETC_DIR/vm.args"}"
+# shellcheck source=ejabberdctl.cfg.example
[ -f "$EJABBERDCTL_CONFIG_PATH" ] && . "$EJABBERDCTL_CONFIG_PATH"
[ -n "$ERLANG_NODE_ARG" ] && ERLANG_NODE="$ERLANG_NODE_ARG"
[ "$ERLANG_NODE" = "${ERLANG_NODE%.*}" ] && S="-s"
@@ -81,7 +84,7 @@ if [ -n "$INET_DIST_INTERFACE" ] ; then
fi
# if vm.args file exists in config directory, pass it to Erlang VM
[ -f "$VMARGS" ] && ERLANG_OPTS="$ERLANG_OPTS -args_file $VMARGS"
-ERL_LIBS={{libdir}}
+ERL_LIBS='{{libdir}}'
ERL_CRASH_DUMP="$LOGS_DIR"/erl_crash_$(date "+%Y%m%d-%H%M%S").dump
ERL_INETRC="$ETC_DIR"/inetrc
@@ -105,6 +108,7 @@ export ERL_MAX_ETS_TABLES
export CONTRIB_MODULES_PATH
export CONTRIB_MODULES_CONF_DIR
export ERL_LIBS
+export SCRIPT_DIR
# run command either directly or via su $INSTALLUSER
exec_cmd()
@@ -128,14 +132,6 @@ exec_iex()
# usage
debugwarning()
{
- if [ "$OSTYPE" != "cygwin" ] && [ "$OSTYPE" != "win32" ] ; then
- if [ "a$TERM" = "a" ] || [ "$TERM" = "dumb" ] ; then
- echo "Terminal type not supported."
- echo "You may have to set the TERM environment variable to fix this."
- exit 8
- fi
- fi
-
if [ "$EJABBERD_BYPASS_WARNINGS" != "true" ] ; then
echo "--------------------------------------------------------------------"
echo ""
@@ -153,7 +149,7 @@ debugwarning()
echo "To bypass permanently this warning, add to ejabberdctl.cfg the line:"
echo " EJABBERD_BYPASS_WARNINGS=true"
echo "Press return to continue"
- read -r input
+ read -r _
echo ""
fi
}
@@ -176,7 +172,7 @@ livewarning()
echo "To bypass permanently this warning, add to ejabberdctl.cfg the line:"
echo " EJABBERD_BYPASS_WARNINGS=true"
echo "Press return to continue"
- read -r input
+ read -r _
echo ""
fi
}
@@ -206,8 +202,9 @@ help()
uid()
{
uuid=$(uuidgen 2>/dev/null)
+ random=$(awk 'BEGIN { srand(); print int(rand()*32768) }' /dev/null)
[ -z "$uuid" ] && [ -f /proc/sys/kernel/random/uuid ] && uuid=$(cat /proc/sys/kernel/random/uuid)
- [ -z "$uuid" ] && uuid=$(printf "%X" "${RANDOM:-$$}$(date +%M%S)")
+ [ -z "$uuid" ] && uuid=$(printf "%X" "${random:-$$}$(date +%M%S)")
uuid=$(printf '%s' $uuid | sed 's/^\(...\).*$/\1/')
[ $# -eq 0 ] && echo "${uuid}-${ERLANG_NODE}"
[ $# -eq 1 ] && echo "${uuid}-${1}-${ERLANG_NODE}"
diff --git a/include/mod_muc_room.hrl b/include/mod_muc_room.hrl
index bbe656575..cc00f73c8 100644
--- a/include/mod_muc_room.hrl
+++ b/include/mod_muc_room.hrl
@@ -65,6 +65,7 @@
captcha_whitelist = (?SETS):empty() :: gb_sets:set(),
mam = false :: boolean(),
pubsub = <<"">> :: binary(),
+ enable_hats = false :: boolean(),
lang = ejabberd_option:language() :: binary()
}).
@@ -124,6 +125,7 @@
history = #lqueue{} :: lqueue(),
subject = [] :: [text()],
subject_author = <<"">> :: binary(),
+ hats_users = #{} :: map(), % FIXME on OTP 21+: #{ljid() => #{binary() => binary()}},
just_created = erlang:system_time(microsecond) :: true | integer(),
activity = treap:empty() :: treap:treap(),
room_shaper = none :: ejabberd_shaper:shaper(),
diff --git a/include/pubsub.hrl b/include/pubsub.hrl
index 8496b95ac..da919e9e2 100644
--- a/include/pubsub.hrl
+++ b/include/pubsub.hrl
@@ -23,7 +23,7 @@
-define(ERR_EXTENDED(E, C), mod_pubsub:extended_error(E, C)).
%% The actual limit can be configured with mod_pubsub's option max_items_node
--define(MAXITEMS, 10).
+-define(MAXITEMS, 1000).
%% this is currently a hard limit.
%% Would be nice to have it configurable.
diff --git a/man/ejabberd.yml.5 b/man/ejabberd.yml.5
index f25f57250..8fb59c381 100644
--- a/man/ejabberd.yml.5
+++ b/man/ejabberd.yml.5
@@ -2,12 +2,12 @@
.\" Title: ejabberd.yml
.\" Author: [see the "AUTHOR" section]
.\" Generator: DocBook XSL Stylesheets v1.79.1 <http://docbook.sf.net/>
-.\" Date: 07/21/2021
+.\" Date: 12/08/2021
.\" Manual: \ \&
.\" Source: \ \&
.\" Language: English
.\"
-.TH "EJABBERD\&.YML" "5" "07/21/2021" "\ \&" "\ \&"
+.TH "EJABBERD\&.YML" "5" "12/08/2021" "\ \&" "\ \&"
.\" -----------------------------------------------------------------
.\" * Define some portability stuff
.\" -----------------------------------------------------------------
@@ -82,7 +82,7 @@ All options can be changed in runtime by running \fIejabberdctl reload\-config\f
.sp
Some options can be specified for particular virtual host(s) only using \fIhost_config\fR or \fIappend_host_config\fR options\&. Such options are called \fIlocal\fR\&. Examples are \fImodules\fR, \fIauth_method\fR and \fIdefault_db\fR\&. The options that cannot be defined per virtual host are called \fIglobal\fR\&. Examples are \fIloglevel\fR, \fIcertfiles\fR and \fIlisten\fR\&. It is a configuration mistake to put \fIglobal\fR options under \fIhost_config\fR or \fIappend_host_config\fR section \- ejabberd will refuse to load such configuration\&.
.sp
-It is not recommended to write ejabberd\&.yml from scratch\&. Instead it is better to start from "default" configuration file available at https://github\&.com/processone/ejabberd/blob/21\&.07/ejabberd\&.yml\&.example\&. Once you get ejabberd running you can start changing configuration options to meet your requirements\&.
+It is not recommended to write ejabberd\&.yml from scratch\&. Instead it is better to start from "default" configuration file available at https://github\&.com/processone/ejabberd/blob/21\&.12/ejabberd\&.yml\&.example\&. Once you get ejabberd running you can start changing configuration options to meet your requirements\&.
.sp
Note that this document is intended to provide comprehensive description of all configuration options that can be consulted to understand the meaning of a particular option, its format and possible values\&. It will be quite hard to understand how to configure ejabberd by reading this document only \- for this purpose the reader is recommended to read online Configuration Guide available at https://docs\&.ejabberd\&.im/admin/configuration\&.
.SH "TOP LEVEL OPTIONS"
@@ -316,14 +316,46 @@ means that the same username can be taken multiple times in anonymous login mode
.PP
\fBanonymous_protocol\fR: \fIlogin_anon | sasl_anon | both\fR
.RS 4
+Define what anonymous protocol will be used:
+.sp
+.RS 4
+.ie n \{\
+\h'-04'\(bu\h'+03'\c
+.\}
+.el \{\
+.sp -1
+.IP \(bu 2.3
+.\}
\fIlogin_anon\fR
means that the anonymous login method will be used\&.
+.RE
+.sp
+.RS 4
+.ie n \{\
+\h'-04'\(bu\h'+03'\c
+.\}
+.el \{\
+.sp -1
+.IP \(bu 2.3
+.\}
\fIsasl_anon\fR
means that the SASL Anonymous method will be used\&.
+.RE
+.sp
+.RS 4
+.ie n \{\
+\h'-04'\(bu\h'+03'\c
+.\}
+.el \{\
+.sp -1
+.IP \(bu 2.3
+.\}
\fIboth\fR
-means that SASL Anonymous and login anonymous are both enabled\&. The default value is
-\fIsasl_anon\fR\&.
+means that SASL Anonymous and login anonymous are both enabled\&.
+.RE
.RE
+.sp
+The default value is \fIsasl_anon\fR\&.
.PP
\fBapi_permissions\fR: \fI[Permission, \&.\&.\&.]\fR
.RS 4
@@ -375,7 +407,7 @@ A list of authentication methods to use\&. If several methods are defined, authe
This is used by the contributed module
\fIejabberd_auth_http\fR
that can be installed from the
-\fIejabberd\-contrib\fR
+ejabberd\-contrib
Git repository\&. Please refer to that module\(cqs README file for details\&.
.RE
.sp
@@ -383,14 +415,36 @@ Git repository\&. Please refer to that module\(cqs README file for details\&.
.PP
\fBauth_password_format\fR: \fIplain | scram\fR
.RS 4
-The option defines in what format the users passwords are stored\&.
+The option defines in what format the users passwords are stored:
+.sp
+.RS 4
+.ie n \{\
+\h'-04'\(bu\h'+03'\c
+.\}
+.el \{\
+.sp -1
+.IP \(bu 2.3
+.\}
\fIplain\fR: The password is stored as plain text in the database\&. This is risky because the passwords can be read if your database gets compromised\&. This is the default value\&. This format allows clients to authenticate using: the old Jabber Non\-SASL (XEP\-0078), SASL PLAIN, SASL DIGEST\-MD5, and SASL SCRAM\-SHA\-1\&.
-\fIscram\fR: The password is not stored, only some information that allows to verify the hash provided by the client\&. It is impossible to obtain the original plain password from the stored information; for this reason, when this value is configured it cannot be changed to plain anymore\&. This format allows clients to authenticate using: SASL PLAIN and SASL SCRAM\-SHA\-1\&.
+.RE
+.sp
+.RS 4
+.ie n \{\
+\h'-04'\(bu\h'+03'\c
+.\}
+.el \{\
+.sp -1
+.IP \(bu 2.3
+.\}
+\fIscram\fR: The password is not stored, only some information that allows to verify the hash provided by the client\&. It is impossible to obtain the original plain password from the stored information; for this reason, when this value is configured it cannot be changed to plain anymore\&. This format allows clients to authenticate using: SASL PLAIN and SASL SCRAM\-SHA\-1\&. The default value is
+\fIplain\fR\&.
+.RE
.RE
.PP
\fBauth_scram_hash\fR: \fIsha | sha256 | sha512\fR
.RS 4
-Hash algorith that should be used to store password in SCRAM format\&. You shouldn\(cqt change this if you already have passwords generated with a different algorithm \- users that have such passwords will not be able to authenticate\&.
+Hash algorith that should be used to store password in SCRAM format\&. You shouldn\(cqt change this if you already have passwords generated with a different algorithm \- users that have such passwords will not be able to authenticate\&. The default value is
+\fIsha\fR\&.
.RE
.PP
\fBauth_use_cache\fR: \fItrue | false\fR
@@ -476,7 +530,7 @@ For server conections, this \fIca_file\fR option is overriden by the s2s_cafile
\fBcache_life_time\fR: \fItimeout()\fR
.RS 4
The time of a cached item to keep in cache\&. Once it\(cqs expired, the corresponding item is erased from cache\&. The default value is
-\fIone hour\fR\&. Several modules have a similar option; and some core ejabberd parts support similar options too, see
+\fI1 hour\fR\&. Several modules have a similar option; and some core ejabberd parts support similar options too, see
\fIauth_cache_life_time\fR,
\fIoauth_cache_life_time\fR,
\fIrouter_cache_life_time\fR, and
@@ -507,7 +561,9 @@ A maximum number of items (not memory!) in cache\&. The rule of thumb, for all t
.PP
\fBcaptcha_cmd\fR: \fIPath\fR
.RS 4
-Full path to a script that generates CAPTCHA images\&. There is no default value: when this option is not set, CAPTCHA functionality is completely disabled\&.
+Full path to a script that generates
+CAPTCHA
+images\&. There is no default value: when this option is not set, CAPTCHA functionality is completely disabled\&.
.RE
.PP
\fBcaptcha_host\fR: \fIString\fR
@@ -519,13 +575,17 @@ instead\&.
.PP
\fBcaptcha_limit\fR: \fIpos_integer() | infinity\fR
.RS 4
-Maximum number of CAPTCHA generated images per minute for any given JID\&. The option is intended to protect the server from CAPTCHA DoS\&. The default value is
+Maximum number of
+CAPTCHA
+generated images per minute for any given JID\&. The option is intended to protect the server from CAPTCHA DoS\&. The default value is
\fIinfinity\fR\&.
.RE
.PP
\fBcaptcha_url\fR: \fIURL\fR
.RS 4
-An URL where CAPTCHA requests should be sent\&. NOTE: you need to configure
+An URL where
+CAPTCHA
+requests should be sent\&. NOTE: you need to configure
\fIrequest_handlers\fR
for
\fIejabberd_http\fR
@@ -815,7 +875,8 @@ Path to the file that contains the JWK Key\&. The default value is
.RS 4
The option defines the default language of server strings that can be seen by XMPP clients\&. If an XMPP client does not possess
\fIxml:lang\fR
-attribute, the specified language is used\&.
+attribute, the specified language is used\&. The default value is
+\fI"en"\fR\&.
.RE
.PP
\fBldap_backups\fR: \fI[Host, \&.\&.\&.]\fR
@@ -948,7 +1009,11 @@ section for details\&.
\fBlog_rotate_count\fR: \fINumber\fR
.RS 4
The number of rotated log files to keep\&. The default value is
-\fI1\fR\&.
+\fI1\fR, which means that only keeps
+ejabberd\&.log\&.0,
+error\&.log\&.0
+and
+crash\&.log\&.0\&.
.RE
.PP
\fBlog_rotate_size\fR: \fIpos_integer() | infinity\fR
@@ -989,8 +1054,7 @@ seconds\&.
.RS 4
This option can be used to tune tick time parameter of
\fInet_kernel\fR\&. It tells Erlang VM how often nodes should check if intra\-node communication was not interrupted\&. This option must have identical value on all nodes, or it will lead to subtle bugs\&. Usually leaving default value of this is option is best, tweak it only if you know what you are doing\&. The default value is
-\fI1\fR
-minute\&.
+\fI1 minute\fR\&.
.RE
.PP
\fBnew_sql_schema\fR: \fItrue | false\fR
@@ -998,7 +1062,7 @@ minute\&.
Whether to use
\fInew\fR
SQL schema\&. All schemas are located at
-https://github\&.com/processone/ejabberd/tree/21\&.07/sql\&. There are two schemas available\&. The default legacy schema allows to store one XMPP domain into one ejabberd database\&. The
+https://github\&.com/processone/ejabberd/tree/21\&.12/sql\&. There are two schemas available\&. The default legacy schema allows to store one XMPP domain into one ejabberd database\&. The
\fInew\fR
schema allows to handle several XMPP domains in a single ejabberd database\&. Using this
\fInew\fR
@@ -1392,8 +1456,7 @@ if the latter is not set\&.
\fBs2s_timeout\fR: \fItimeout()\fR
.RS 4
A time to wait before closing an idle s2s connection\&. The default value is
-\fI10\fR
-minutes\&.
+\fI10 minutes\fR\&.
.RE
.PP
\fBs2s_tls_compression\fR: \fItrue | false\fR
@@ -1795,24 +1858,7 @@ Details for some commands:
\fIsrg\-create\fR: If you want to put a group Name with blankspaces, use the characters "\*(Aq and \*(Aq" to define when the Name starts and ends\&. See an example below\&.
.RE
.sp
-.it 1 an-trap
-.nr an-no-space-flag 1
-.nr an-break-flag 1
-.br
-.ps +1
-\fBAvailable options:\fR
-.RS 4
-.PP
-\fBmodule_resource\fR: \fIResource\fR
-.RS 4
-Indicate the resource that the XMPP stanzas must use in the FROM or TO JIDs\&. This is only useful in the
-\fIget_vcard*\fR
-and
-\fIset_vcard*\fR
-commands\&. The default value is
-\fImod_admin_extra\fR\&.
-.RE
-.RE
+The module has no options\&.
.sp
.it 1 an-trap
.nr an-no-space-flag 1
@@ -1835,8 +1881,7 @@ access_rules:
vcard_set:
\- allow: adminextraresource
modules:
- mod_admin_extra:
- module_resource: "modadminextraf8x,31ad"
+ mod_admin_extra: {}
mod_vcard:
access_set: vcard_set
.fi
@@ -1884,7 +1929,7 @@ ejabberdctl srg\-create g1 example\&.org "\*(AqGroup number 1\*(Aq" this_is_g1 g
.RE
.SS "mod_admin_update_sql"
.sp
-This module can be used to update existing SQL database from the default to the new schema\&. Check the section Default and New Schemas for details\&. Please note that only PostgreSQL is supported\&. When the module is loaded use \fIupdate_sql\fR ejabberdctl command\&.
+This module can be used to update existing SQL database from the default to the new schema\&. Check the section Default and New Schemas for details\&. Please note that only PostgreSQL is supported\&. When the module is loaded use \fIupdate_sql\fR API\&.
.sp
The module has no options\&.
.SS "mod_announce"
@@ -2321,6 +2366,78 @@ While a client is inactive, queue presence stanzas that indicate (un)availabilit
The module provides server configuration functionality via XEP\-0050: Ad\-Hoc Commands\&. This module requires \fImod_adhoc\fR to be loaded\&.
.sp
The module has no options\&.
+.SS "mod_conversejs"
+.sp
+This module serves a simple page for the Converse XMPP web browser client\&.
+.sp
+To use this module, in addition to adding it to the \fImodules\fR section, you must also enable it in \fIlisten\fR → \fIejabberd_http\fR → request_handlers\&.
+.sp
+You must also setup either the option \fIwebsocket_url\fR or \fIbosh_service_url\fR\&.
+.sp
+By default, the options \fIconversejs_css\fR and \fIconversejs_script\fR point to the public Converse\&.js client\&. Alternatively, you can host the client locally using \fImod_http_fileserver\fR\&.
+.sp
+.it 1 an-trap
+.nr an-no-space-flag 1
+.nr an-break-flag 1
+.br
+.ps +1
+\fBAvailable options:\fR
+.RS 4
+.PP
+\fBbosh_service_url\fR: \fIBoshURL\fR
+.RS 4
+BOSH service URL to which Converse\&.js can connect to\&.
+.RE
+.PP
+\fBconversejs_css\fR: \fIURL\fR
+.RS 4
+Converse\&.js CSS URL\&.
+.RE
+.PP
+\fBconversejs_script\fR: \fIURL\fR
+.RS 4
+Converse\&.js main script URL\&.
+.RE
+.PP
+\fBdefault_domain\fR: \fIDomain\fR
+.RS 4
+Specify a domain to act as the default for user JIDs\&. The default value is the first domain defined in the ejabberd configuration file\&.
+.RE
+.PP
+\fBwebsocket_url\fR: \fIWebsocketURL\fR
+.RS 4
+A websocket URL to which Converse\&.js can connect to\&.
+.RE
+.RE
+.sp
+.it 1 an-trap
+.nr an-no-space-flag 1
+.nr an-break-flag 1
+.br
+.ps +1
+\fBExample:\fR
+.RS 4
+.sp
+.if n \{\
+.RS 4
+.\}
+.nf
+listen:
+ \-
+ port: 5280
+ module: ejabberd_http
+ request_handlers:
+ /websocket: ejabberd_http_ws
+ /conversejs: mod_conversejs
+
+modules:
+ mod_conversejs:
+ websocket_url: "ws://example\&.org:5280/websocket"
+.fi
+.if n \{\
+.RE
+.\}
+.RE
.SS "mod_delegation"
.sp
This module is an implementation of XEP\-0355: Namespace Delegation\&. Only admin mode has been implemented by now\&. Namespace delegation allows external services to handle IQ using specific namespace\&. This may be applied for external PEP service\&.
@@ -2479,11 +2596,11 @@ server_info:
\-
modules: all
name: abuse\-addresses
- urls: [mailto:abuse@shakespeare\&.lit]
+ urls: ["mailto:abuse@shakespeare\&.lit"]
\-
modules: [mod_muc]
name: "Web chatroom logs"
- urls: [http://www\&.example\&.org/muc\-logs]
+ urls: ["http://www\&.example\&.org/muc\-logs"]
\-
modules: [mod_disco]
name: feedback\-addresses
@@ -2563,13 +2680,40 @@ The number of C2S authentication failures to trigger the IP ban\&. The default v
.sp
This module provides a ReST API to call ejabberd commands using JSON data\&.
.sp
-To use this module, in addition to adding it to the \fImodules\fR section, you must also add it to \fIrequest_handlers\fR of some listener\&.
+To use this module, in addition to adding it to the \fImodules\fR section, you must also enable it in \fIlisten\fR → \fIejabberd_http\fR → request_handlers\&.
.sp
To use a specific API version N, when defining the URL path in the request_handlers, add a \fIvN\fR\&. For example: \fI/api/v2: mod_http_api\fR
.sp
To run a command, send a POST request to the corresponding URL: \fIhttp://localhost:5280/api/<command_name>\fR
.sp
The module has no options\&.
+.sp
+.it 1 an-trap
+.nr an-no-space-flag 1
+.nr an-break-flag 1
+.br
+.ps +1
+\fBExample:\fR
+.RS 4
+.sp
+.if n \{\
+.RS 4
+.\}
+.nf
+listen:
+ \-
+ port: 5280
+ module: ejabberd_http
+ request_handlers:
+ /api: mod_http_api
+
+modules:
+ mod_http_api: {}
+.fi
+.if n \{\
+.RE
+.\}
+.RE
.SS "mod_http_fileserver"
.sp
This simple module serves files from the local disk over HTTP\&.
@@ -2697,7 +2841,7 @@ modules:
.sp
This module allows for requesting permissions to upload a file via HTTP as described in XEP\-0363: HTTP File Upload\&. If the request is accepted, the client receives a URL for uploading the file and another URL from which that file can later be downloaded\&.
.sp
-In order to use this module, it must be configured as a \fIrequest_handler\fR for \fIejabberd_http\fR listener\&.
+In order to use this module, it must be enabled in \fIlisten\fR → \fIejabberd_http\fR → request_handlers\&.
.sp
.it 1 an-trap
.nr an-no-space-flag 1
@@ -2746,7 +2890,9 @@ This option defines the permission bits of uploaded files\&. The bits are specif
.PP
\fBget_url\fR: \fIURL\fR
.RS 4
-This option specifies the initial part of the GET URLs used for downloading the files\&. By default, it is set to the same value as
+This option specifies the initial part of the GET URLs used for downloading the files\&. The default value is
+\fIundefined\fR\&. When this option is
+\fIundefined\fR, this option is set to the same value as
\fIput_url\fR\&. The keyword @HOST@ is replaced with the virtual host name\&. NOTE: if GET requests are handled by
\fImod_http_upload\fR, the
\fIget_url\fR
@@ -2795,7 +2941,7 @@ A name of the service in the Service Discovery\&. This will only be displayed by
.PP
\fBput_url\fR: \fIURL\fR
.RS 4
-This option specifies the initial part of the PUT URLs used for file uploads\&. The keyword @HOST@ is replaced with the virtual host name\&. NOTE: different virtual hosts cannot use the same PUT URL\&. The default value is "https://@HOST@:5443"\&.
+This option specifies the initial part of the PUT URLs used for file uploads\&. The keyword @HOST@ is replaced with the virtual host name\&. NOTE: different virtual hosts cannot use the same PUT URL\&. The default value is "https://@HOST@:5443/upload"\&.
.RE
.PP
\fBrm_on_unregister\fR: \fItrue | false\fR
@@ -3530,21 +3676,24 @@ This option specifies who is allowed to administrate the Multi\-User Chat servic
.PP
\fBaccess_create\fR: \fIAccessName\fR
.RS 4
-To configure who is allowed to create new rooms at the Multi\-User Chat service, this option can be used\&. By default any account in the local ejabberd server is allowed to create rooms\&.
+To configure who is allowed to create new rooms at the Multi\-User Chat service, this option can be used\&. The default value is
+\fIall\fR, which means everyone is allowed to create rooms\&.
.RE
.PP
\fBaccess_mam\fR: \fIAccessName\fR
.RS 4
To configure who is allowed to modify the
\fImam\fR
-room option\&. By default any account in the local ejabberd server is allowed to modify that option\&.
+room option\&. The default value is
+\fIall\fR, which means everyone is allowed to modify that option\&.
.RE
.PP
\fBaccess_persistent\fR: \fIAccessName\fR
.RS 4
To configure who is allowed to modify the
\fIpersistent\fR
-room option\&. By default any account in the local ejabberd server is allowed to modify that option\&.
+room option\&. The default value is
+\fIall\fR, which means everyone is allowed to modify that option\&.
.RE
.PP
\fBaccess_register\fR: \fIAccessName\fR
@@ -3825,7 +3974,8 @@ This option defines the number of service admins or room owners allowed to enter
.PP
\fBmax_users_presence\fR: \fINumber\fR
.RS 4
-This option defines after how many users in the room, it is considered overcrowded\&. When a MUC room is considered overcrowed, presence broadcasts are limited to reduce load, traffic and excessive presence "storm" received by participants\&.
+This option defines after how many users in the room, it is considered overcrowded\&. When a MUC room is considered overcrowed, presence broadcasts are limited to reduce load, traffic and excessive presence "storm" received by participants\&. The default value is
+\fI1000\fR\&.
.RE
.PP
\fBmin_message_interval\fR: \fINumber\fR
@@ -4498,8 +4648,7 @@ This module implements support for XEP\-0199: XMPP Ping and periodic keepalives\
\fBping_ack_timeout\fR: \fItimeout()\fR
.RS 4
How long to wait before deeming that a client has not answered a given server ping request\&. The default value is
-\fI32\fR
-seconds\&.
+\fIundefined\fR\&.
.RE
.PP
\fBping_interval\fR: \fItimeout()\fR
@@ -4531,7 +4680,7 @@ means destroying the underlying connection,
\fInone\fR
means to do nothing\&. NOTE: when
\fImod_stream_mgmt\fR
-module is loaded and stream management is enabled by a client, killing the client connection doesn\(cqt mean killing the client session \- the session will be kept alive in order to give the client a chance to resume it\&. The default value is
+is loaded and stream management is enabled by a client, killing the client connection doesn\(cqt mean killing the client session \- the session will be kept alive in order to give the client a chance to resume it\&. The default value is
\fInone\fR\&.
.RE
.RE
@@ -5136,10 +5285,16 @@ or
\fIfalse\fR\&. If not defined, pubsub does not cache last items\&. On systems with not so many nodes, caching last items speeds up pubsub and allows to raise user connection rate\&. The cost is memory usage, as every item is stored in memory\&.
.RE
.PP
-\fBmax_items_node\fR: \fIMaxItems\fR
+\fBmax_item_expire_node\fR: \fItimeout() | infinity\fR
+.RS 4
+Specify the maximum item epiry time\&. Default value is:
+\fIinfinity\fR\&.
+.RE
+.PP
+\fBmax_items_node\fR: \fInon_neg_integer() | infinity\fR
.RS 4
Define the maximum number of items that can be stored in a node\&. Default value is:
-\fI10\fR\&.
+\fI1000\fR\&.
.RE
.PP
\fBmax_nodes_discoitems\fR: \fIpos_integer() | infinity\fR
@@ -5437,9 +5592,9 @@ The module depends on \fImod_push\fR\&.
.PP
\fBresume_timeout\fR: \fItimeout()\fR
.RS 4
-This option specifies the period of time until the session of a disconnected push client times out\&. This timeout is only in effect as long as no push notification is issued\&. Once that happened, the resumption timeout configured for the
+This option specifies the period of time until the session of a disconnected push client times out\&. This timeout is only in effect as long as no push notification is issued\&. Once that happened, the resumption timeout configured for
\fImod_stream_mgmt\fR
-module is restored\&. The default value is
+is restored\&. The default value is
\fI72\fR
hours\&.
.RE
@@ -5499,7 +5654,7 @@ Change the password from an existing account on the server\&.
Delete an existing account on the server\&.
.RE
.sp
-This module reads also another option defined globally for the server: \fIregistration_timeout\fR\&. Please check that option documentation in the section with top\-level options\&.
+This module reads also the top\-level \fIregistration_timeout\fR option defined globally for the server, so please check that option documentation too\&.
.sp
.it 1 an-trap
.nr an-no-space-flag 1
@@ -5528,11 +5683,18 @@ doesn\(cqt allow to register new accounts from s2s or existing c2s sessions\&. Y
Specify rules to restrict access for user unregistration\&. By default any user is able to unregister their account\&.
.RE
.PP
+\fBallow_modules\fR: \fIall | [Module, \&.\&.\&.]\fR
+.RS 4
+List of modules that can register accounts, or
+\fIall\fR\&. The default value is
+\fIall\fR, which is equivalent to something like
+\fI[mod_register, mod_register_web]\fR\&.
+.RE
+.PP
\fBcaptcha_protected\fR: \fItrue | false\fR
.RS 4
-Protect registrations with CAPTCHA (see section
-CAPTCHA
-of the Configuration Guide)\&. The default is
+Protect registrations with
+CAPTCHA\&. The default is
\fIfalse\fR\&.
.RE
.PP
@@ -5551,7 +5713,8 @@ This option sets the minimum
Shannon entropy
for passwords\&. The value
\fIEntropy\fR
-is a number of bits of entropy\&. The recommended minimum is 32 bits\&. The default is 0, i\&.e\&. no checks are performed\&.
+is a number of bits of entropy\&. The recommended minimum is 32 bits\&. The default is
+\fI0\fR, i\&.e\&. no checks are performed\&.
.RE
.PP
\fBredirect_url\fR: \fIURL\fR
@@ -5610,13 +5773,40 @@ Change the password from an existing account on the server\&.
Unregister an existing account on the server\&.
.RE
.sp
-This module supports CAPTCHA image to register a new account\&. To enable this feature, configure the options \fIcaptcha_cmd\fR and \fIcaptcha_url\fR, which are documented in the section with top\-level options\&.
+This module supports CAPTCHA to register a new account\&. To enable this feature, configure the top\-level \fIcaptcha_cmd\fR and top\-level \fIcaptcha_url\fR options\&.
.sp
-As an example usage, the users of the host \fIexample\&.org\fR can visit the page: \fIhttps://example\&.org:5281/register/\fR It is important to include the last / character in the URL, otherwise the subpages URL will be incorrect\&.
+As an example usage, the users of the host \fIlocalhost\fR can visit the page: \fIhttps://localhost:5280/register/\fR It is important to include the last / character in the URL, otherwise the subpages URL will be incorrect\&.
.sp
-The module depends on \fImod_register\fR where all the configuration is performed\&.
+This module is enabled in \fIlisten\fR → \fIejabberd_http\fR → request_handlers, no need to enable in \fImodules\fR\&. The module depends on \fImod_register\fR where all the configuration is performed\&.
.sp
The module has no options\&.
+.sp
+.it 1 an-trap
+.nr an-no-space-flag 1
+.nr an-break-flag 1
+.br
+.ps +1
+\fBExample:\fR
+.RS 4
+.sp
+.if n \{\
+.RS 4
+.\}
+.nf
+listen:
+ \-
+ port: 5280
+ module: ejabberd_http
+ request_handlers:
+ /register: mod_register_web
+
+modules:
+ mod_register: {}
+.fi
+.if n \{\
+.RE
+.\}
+.RE
.SS "mod_roster"
.sp
This module implements roster management as defined in RFC6121 Section 2\&. The module also adds support for XEP\-0237: Roster Versioning\&.
@@ -5910,8 +6100,9 @@ option, but applied to this module only\&.
.PP
\fBdb_type\fR: \fImnesia | sql\fR
.RS 4
-Define the type of storage where the module will create the tables and store user information\&. The default is the storage defined by the global option
-\fIdefault_db\fR, or
+Define the type of storage where the module will create the tables and store user information\&. The default is the storage defined by the top\-level
+\fIdefault_db\fR
+option, or
\fImnesia\fR
if omitted\&. If
\fIsql\fR
@@ -6521,7 +6712,8 @@ minute\&.
.RS 4
Same as top\-level
\fIcache_life_time\fR
-option, but applied to this module only\&.
+option, but applied to this module only\&. The default value is
+\fI48 hours\fR\&.
.RE
.PP
\fBcache_size\fR: \fIpos_integer() | infinity\fR
@@ -6594,8 +6786,7 @@ This option defines which access rule will be used to control who is allowed to
\fBcredentials_lifetime\fR: \fItimeout()\fR
.RS 4
The lifetime of temporary credentials offered to clients\&. If ejabberd\(cqs built\-in TURN service is used, TURN relays allocated using temporary credentials will be terminated shortly after the credentials expired\&. The default value is
-\fI12\fR
-hours\&. Note that restarting the ejabberd node invalidates any temporary credentials offered before the restart unless a
+\fI12 hours\fR\&. Note that restarting the ejabberd node invalidates any temporary credentials offered before the restart unless a
\fIsecret\fR
is specified (see below)\&.
.RE
@@ -7121,7 +7312,7 @@ The module depends on \fImod_vcard\fR\&.
.ps -1
.br
.sp
-Nowadays XEP\-0153 is used mostly as "read\-only", i\&.e\&. modern clients don\(cqt publish their avatars inside vCards\&. Thus in the majority of cases the module is only used along with \fImod_avatar\fR module for providing backward compatibility\&.
+Nowadays XEP\-0153 is used mostly as "read\-only", i\&.e\&. modern clients don\(cqt publish their avatars inside vCards\&. Thus in the majority of cases the module is only used along with \fImod_avatar\fR for providing backward compatibility\&.
.sp .5v
.RE
.sp
@@ -7189,13 +7380,13 @@ TODO
ProcessOne\&.
.SH "VERSION"
.sp
-This document describes the configuration file of ejabberd 21\&.04\&.131\&. Configuration options of other ejabberd versions may differ significantly\&.
+This document describes the configuration file of ejabberd 21\&.12\&. Configuration options of other ejabberd versions may differ significantly\&.
.SH "REPORTING BUGS"
.sp
Report bugs to https://github\&.com/processone/ejabberd/issues
.SH "SEE ALSO"
.sp
-Default configuration file: https://github\&.com/processone/ejabberd/blob/21\&.07/ejabberd\&.yml\&.example
+Default configuration file: https://github\&.com/processone/ejabberd/blob/21\&.12/ejabberd\&.yml\&.example
.sp
Main site: https://ejabberd\&.im
.sp
diff --git a/mix.exs b/mix.exs
index 0b5d40b09..64f94345c 100644
--- a/mix.exs
+++ b/mix.exs
@@ -24,6 +24,7 @@ defmodule Ejabberd.MixProject do
case config(:vsn) do
:false -> "0.0.0" # ./configure wasn't run: vars.config not created
'0.0' -> "0.0.0" # the full git repository wasn't downloaded
+ 'latest.0' -> "0.0.0" # running 'docker-ejabberd/ecs/build.sh latest'
[_, _, ?., _, _] = x ->
head = String.replace(:erlang.list_to_binary(x), ~r/0+([0-9])/, "\\1")
<<head::binary, ".0">>
@@ -87,6 +88,7 @@ defmodule Ejabberd.MixProject do
if_version_below('23', [{:d, :USE_OLD_PG2}]) ++
if_version_below('24', [{:d, :COMPILER_REPORTS_ONLY_LINES}]) ++
if_version_below('24', [{:d, :SYSTOOLS_APP_DEF_WITHOUT_OPTIONAL}]) ++
+ if_function_exported(:uri_string, :normalize, 1, [{:d, :HAVE_URI_STRING}])
if_function_exported(:erl_error, :format_exception, 6, [{:d, :HAVE_ERL_ERROR}])
defines = for {:d, value} <- result, do: {:d, value}
result ++ [{:d, :ALL_DEFS, defines}]
@@ -102,7 +104,7 @@ defmodule Ejabberd.MixProject do
end
defp deps do
- [{:base64url, "~> 0.0.1"},
+ [{:base64url, "~> 1.0"},
{:cache_tab, "~> 1.0"},
{:distillery, "~> 2.0"},
{:eimp, "~> 1.0"},
@@ -113,7 +115,7 @@ defmodule Ejabberd.MixProject do
{:fast_yaml, "~> 1.0"},
{:idna, "~> 6.0"},
{:jiffy, "~> 1.0.5"},
- {:jose, "~> 1.8"},
+ {:jose, "~> 1.11.1"},
{:lager, "~> 3.9.1"},
{:mqtree, "~> 1.0"},
{:p1_acme, "~> 1.0"},
@@ -124,7 +126,7 @@ defmodule Ejabberd.MixProject do
{:pkix, "~> 1.0"},
{:stringprep, ">= 1.0.26"},
{:stun, "~> 1.0"},
- {:xmpp, git: "https://github.com/processone/xmpp", ref: "e943c0285aa85e3cbd4bfb9259f6b7de32b00395", override: true},
+ {:xmpp, "~> 1.5"},
{:yconf, "~> 1.0"}]
++ cond_deps()
end
@@ -145,7 +147,7 @@ defmodule Ejabberd.MixProject do
for {:true, dep} <- [{config(:pam), {:epam, "~> 1.0"}},
{config(:redis), {:eredis, "~> 1.2.0"}},
{config(:zlib), {:ezlib, "~> 1.0"}},
- {config(:lua), {:luerl, "~> 0.3.1"}},
+ {config(:lua), {:luerl, "~> 1.0"}},
{config(:sqlite), {:sqlite3, "~> 1.1"}}], do:
dep
end
@@ -213,6 +215,7 @@ defmodule Ejabberd.MixProject do
epmd: config(:epmd),
bindir: Path.join([config(:release_dir), "releases", version()]),
release_dir: config(:release_dir),
+ erts_dir: config(:erts_dir),
erts_vsn: "erts-#{release.erts_version}"
]
ro = "rel/overlays"
@@ -238,7 +241,9 @@ defmodule Ejabberd.MixProject do
execute.("sed -e 's|{{\\(\[_a-z\]*\\)}}|<%= @\\1 %>|g' ejabberdctl.template > ejabberdctl.example1")
Mix.Generator.copy_template("ejabberdctl.example1", "ejabberdctl.example2", assigns)
- execute.("sed -e 's|{{\\(\[_a-z\]*\\)}}|<%= @\\1 %>|g' ejabberdctl.example2 > ejabberdctl.example3")
+ execute.("sed -e 's|{{\\(\[_a-z\]*\\)}}|<%= @\\1 %>|g' ejabberdctl.example2> ejabberdctl.example2a")
+ Mix.Generator.copy_template("ejabberdctl.example2a", "ejabberdctl.example2b", assigns)
+ execute.("sed -e 's|{{\\(\[_a-z\]*\\)}}|<%= @\\1 %>|g' ejabberdctl.example2b > ejabberdctl.example3")
execute.("sed -e 's|ERLANG_NODE=ejabberd@localhost|ERLANG_NODE=ejabberd|g' ejabberdctl.example3 > ejabberdctl.example4")
execute.("sed -e 's|INSTALLUSER=|ERL_OPTIONS=\"-setcookie \\$\\(cat \"\\${SCRIPT_DIR%/*}/releases/COOKIE\")\"\\nINSTALLUSER=|g' ejabberdctl.example4 > ejabberdctl.example5")
Mix.Generator.copy_template("ejabberdctl.example5", "#{ro}/bin/ejabberdctl", assigns)
@@ -246,6 +251,8 @@ defmodule Ejabberd.MixProject do
File.rm("ejabberdctl.example1")
File.rm("ejabberdctl.example2")
+ File.rm("ejabberdctl.example2a")
+ File.rm("ejabberdctl.example2b")
File.rm("ejabberdctl.example3")
File.rm("ejabberdctl.example4")
File.rm("ejabberdctl.example5")
diff --git a/mix.lock b/mix.lock
index a5b158e0f..d0edb619d 100644
--- a/mix.lock
+++ b/mix.lock
@@ -1,36 +1,36 @@
%{
"artificery": {:hex, :artificery, "0.4.3", "0bc4260f988dcb9dda4b23f9fc3c6c8b99a6220a331534fdf5bf2fd0d4333b02", [:mix], [], "hexpm", "12e95333a30e20884e937abdbefa3e7f5e05609c2ba8cf37b33f000b9ffc0504"},
- "base64url": {:hex, :base64url, "0.0.1", "36a90125f5948e3afd7be97662a1504b934dd5dac78451ca6e9abf85a10286be", [:rebar], [], "hexpm", "fab09b20e3f5db886725544cbcf875b8e73ec93363954eb8a1a9ed834aa8c1f9"},
+ "base64url": {:hex, :base64url, "1.0.1", "f8c7f2da04ca9a5d0f5f50258f055e1d699f0e8bf4cfdb30b750865368403cf6", [:rebar3], [], "hexpm", "f9b3add4731a02a9b0410398b475b33e7566a695365237a6bdee1bb447719f5c"},
"cache_tab": {:hex, :cache_tab, "1.0.29", "6c161988620b788d8df28c8f6af557571609c8e4b671dbadab295a4722cd501b", [:rebar3], [{:p1_utils, "1.0.23", [hex: :p1_utils, repo: "hexpm", optional: false]}], "hexpm", "a02a638021cce91ed1a8628dcbb4795bf5c01c9d11db8c613065923142824ce9"},
"distillery": {:hex, :distillery, "2.1.1", "f9332afc2eec8a1a2b86f22429e068ef35f84a93ea1718265e740d90dd367814", [:mix], [{:artificery, "~> 0.2", [hex: :artificery, repo: "hexpm", optional: false]}], "hexpm", "bbc7008b0161a6f130d8d903b5b3232351fccc9c31a991f8fcbf2a12ace22995"},
- "earmark_parser": {:hex, :earmark_parser, "1.4.13", "0c98163e7d04a15feb62000e1a891489feb29f3d10cb57d4f845c405852bbef8", [:mix], [], "hexpm", "d602c26af3a0af43d2f2645613f65841657ad6efc9f0e361c3b6c06b578214ba"},
+ "earmark_parser": {:hex, :earmark_parser, "1.4.17", "6f3c7e94170377ba45241d394389e800fb15adc5de51d0a3cd52ae766aafd63f", [:mix], [], "hexpm", "f93ac89c9feca61c165b264b5837bf82344d13bebc634cd575cb711e2e342023"},
"eimp": {:hex, :eimp, "1.0.21", "2e918a5dc9a1959ef8713a2360499e3baeee64cfd7881bd9d1f361ca9ddf07e8", [:rebar3], [{:p1_utils, "1.0.23", [hex: :p1_utils, repo: "hexpm", optional: false]}], "hexpm", "998f58538f58aa0cff103414994d7ce56dc253e6576cd6fb40c1ead64aa73a28"},
"epam": {:hex, :epam, "1.0.12", "2a5625d4133bca4b3943791a3f723ba764455a461ae9b6ba5debb262efcf4b40", [:rebar3], [], "hexpm", "54c166c4459cef72f2990a3d89a8f0be27180fe0ab0f24b28ddcc3b815f49f7f"},
- "esip": {:hex, :esip, "1.0.43", "1cbdc073073f80b9b50e2759f66ca13a353eb4f874bcf92501bd4cd767e34d46", [:rebar3], [{:fast_tls, "1.1.13", [hex: :fast_tls, repo: "hexpm", optional: false]}, {:p1_utils, "1.0.23", [hex: :p1_utils, repo: "hexpm", optional: false]}, {:stun, "1.0.44", [hex: :stun, repo: "hexpm", optional: false]}], "hexpm", "b2c758ae52c4588e0399c0b4ce550bfa56551a5a2f828a28389f2614797e4f4b"},
- "ex_doc": {:hex, :ex_doc, "0.25.0", "4070a254664ee5495c2f7cce87c2f43064a8752f7976f2de4937b65871b05223", [:mix], [{:earmark_parser, "~> 1.4.0", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "2d90883bd4f3d826af0bde7fea733a4c20adba1c79158e2330f7465821c8949b"},
+ "esip": {:hex, :esip, "1.0.45", "2f21fb9750f7a505e6bbd43f6d48b0e879b808aba6c2224686c83f2bcd7a34bf", [:rebar3], [{:fast_tls, "1.1.13", [hex: :fast_tls, repo: "hexpm", optional: false]}, {:p1_utils, "1.0.23", [hex: :p1_utils, repo: "hexpm", optional: false]}, {:stun, "1.0.47", [hex: :stun, repo: "hexpm", optional: false]}], "hexpm", "1f1eae69f2bd8d75f42c048409eabb4e3dc71ab6412fc5d998edbdade6ad5f75"},
+ "ex_doc": {:hex, :ex_doc, "0.26.0", "1922164bac0b18b02f84d6f69cab1b93bc3e870e2ad18d5dacb50a9e06b542a3", [:mix], [{:earmark_parser, "~> 1.4.0", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "2775d66e494a9a48355db7867478ffd997864c61c65a47d31c4949459281c78d"},
"ezlib": {:hex, :ezlib, "1.0.10", "c1c24eb18944cfde55f0574e9922d5b0392fa864282f769f82b2ea15e54f6003", [:rebar3], [], "hexpm", "1d317f1d85373686199eb3b4164d3477e95033ac68e45a95ba18e7b7a8c23241"},
"fast_tls": {:hex, :fast_tls, "1.1.13", "828cdc75e1e8fce8158846d2b971d8b4fe2b2ddcc75b759e88d751079bf78afd", [:rebar3], [{:p1_utils, "1.0.23", [hex: :p1_utils, repo: "hexpm", optional: false]}], "hexpm", "d1f422af40c7777fe534496f508ee86515cb929ad10f7d1d56aa94ce899b44a0"},
- "fast_xml": {:hex, :fast_xml, "1.1.47", "bd1d6c081b69c7bce0d2f22b013c1b864ed2588d48f34e2156d9428f8f772c66", [:rebar3], [{:p1_utils, "1.0.23", [hex: :p1_utils, repo: "hexpm", optional: false]}], "hexpm", "dd014c45247498effb9a28cf98cb716db79be635ad1e98c951240763119f24c7"},
+ "fast_xml": {:hex, :fast_xml, "1.1.48", "d41d14015227999a2367264cc97ac1e6770285aab1dc69545ac4f822be01a2d2", [:rebar3], [{:p1_utils, "1.0.23", [hex: :p1_utils, repo: "hexpm", optional: false]}], "hexpm", "afcf9b808c77599395d4bd22ed4560b3d82aa1a24ff5b65f3930fe72a423b3cf"},
"fast_yaml": {:hex, :fast_yaml, "1.0.32", "43f53a2c8572f2e4d66cd4e787fc6761b1c65b9132e42c511d8b9540b0989d65", [:rebar3], [{:p1_utils, "1.0.23", [hex: :p1_utils, repo: "hexpm", optional: false]}], "hexpm", "7258e322739ff0824237ebe44cd158e0bf52cd27a15fe731cf92f4b4c70b913e"},
"goldrush": {:hex, :goldrush, "0.1.9", "f06e5d5f1277da5c413e84d5a2924174182fb108dabb39d5ec548b27424cd106", [:rebar3], [], "hexpm", "99cb4128cffcb3227581e5d4d803d5413fa643f4eb96523f77d9e6937d994ceb"},
"idna": {:hex, :idna, "6.0.0", "689c46cbcdf3524c44d5f3dde8001f364cd7608a99556d8fbd8239a5798d4c10", [:rebar3], [{:unicode_util_compat, "0.4.1", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "4bdd305eb64e18b0273864920695cb18d7a2021f31a11b9c5fbcd9a253f936e2"},
"jiffy": {:hex, :jiffy, "1.0.5", "a69b58faf7123534c20e1b0b7ae97ac52079ca02ed4b6989b4b380179cd63a54", [:rebar3], [], "hexpm", "b617a53f46ae84f20d0c38951367dc947a2cf8cff922aa5c6ac6b64b8b052289"},
- "jose": {:hex, :jose, "1.9.0", "4167c5f6d06ffaebffd15cdb8da61a108445ef5e85ab8f5a7ad926fdf3ada154", [:mix, :rebar3], [{:base64url, "~> 0.0.1", [hex: :base64url, repo: "hexpm", optional: false]}], "hexpm", "6429c4fee52b2dda7861ee19a4f09c8c1ffa213bee3a1ec187828fde95d447ed"},
+ "jose": {:hex, :jose, "1.11.1", "59da64010c69aad6cde2f5b9248b896b84472e99bd18f246085b7b9fe435dcdb", [:mix, :rebar3], [], "hexpm", "078f6c9fb3cd2f4cfafc972c814261a7d1e8d2b3685c0a76eb87e158efff1ac5"},
"lager": {:hex, :lager, "3.9.2", "4cab289120eb24964e3886bd22323cb5fefe4510c076992a23ad18cf85413d8c", [:rebar3], [{:goldrush, "0.1.9", [hex: :goldrush, repo: "hexpm", optional: false]}], "hexpm", "7f904d9e87a8cb7e66156ed31768d1c8e26eba1d54f4bc85b1aa4ac1f6340c28"},
"makeup": {:hex, :makeup, "1.0.5", "d5a830bc42c9800ce07dd97fa94669dfb93d3bf5fcf6ea7a0c67b2e0e4a7f26c", [:mix], [{:nimble_parsec, "~> 0.5 or ~> 1.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "cfa158c02d3f5c0c665d0af11512fed3fba0144cf1aadee0f2ce17747fba2ca9"},
- "makeup_elixir": {:hex, :makeup_elixir, "0.15.1", "b5888c880d17d1cc3e598f05cdb5b5a91b7b17ac4eaf5f297cb697663a1094dd", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.1", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "db68c173234b07ab2a07f645a5acdc117b9f99d69ebf521821d89690ae6c6ec8"},
+ "makeup_elixir": {:hex, :makeup_elixir, "0.15.2", "dc72dfe17eb240552857465cc00cce390960d9a0c055c4ccd38b70629227e97c", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.1", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "fd23ae48d09b32eff49d4ced2b43c9f086d402ee4fd4fcb2d7fad97fa8823e75"},
"makeup_erlang": {:hex, :makeup_erlang, "0.1.1", "3fcb7f09eb9d98dc4d208f49cc955a34218fc41ff6b84df7c75b3e6e533cc65f", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "174d0809e98a4ef0b3309256cbf97101c6ec01c4ab0b23e926a9e17df2077cbb"},
"mqtree": {:hex, :mqtree, "1.0.14", "d201a79b51a9232b80e764b4b77a866f7c30a90c7ac6205d71f391eb3ea7eb31", [:rebar3], [{:p1_utils, "1.0.23", [hex: :p1_utils, repo: "hexpm", optional: false]}], "hexpm", "8626dac5e862b575eaf4836f0fc1be5a7c8435c378c5a309e34ee012d48b6f6e"},
- "nimble_parsec": {:hex, :nimble_parsec, "1.1.0", "3a6fca1550363552e54c216debb6a9e95bd8d32348938e13de5eda962c0d7f89", [:mix], [], "hexpm", "08eb32d66b706e913ff748f11694b17981c0b04a33ef470e33e11b3d3ac8f54b"},
- "p1_acme": {:hex, :p1_acme, "1.0.13", "fec71df416004ce49e295f4846fe5ba3478b41fbe4f73a06b4a8fbc967d6e659", [:rebar3], [{:idna, "6.0.0", [hex: :idna, repo: "hexpm", optional: false]}, {:jiffy, "1.0.5", [hex: :jiffy, repo: "hexpm", optional: false]}, {:jose, "1.9.0", [hex: :jose, repo: "hexpm", optional: false]}, {:yconf, "1.0.12", [hex: :yconf, repo: "hexpm", optional: false]}], "hexpm", "a2ce9d4904304df020c8e92e8577e0fc88f32623540656317c7e25440b4ac8d2"},
+ "nimble_parsec": {:hex, :nimble_parsec, "1.2.0", "b44d75e2a6542dcb6acf5d71c32c74ca88960421b6874777f79153bbbbd7dccc", [:mix], [], "hexpm", "52b2871a7515a5ac49b00f214e4165a40724cf99798d8e4a65e4fd64ebd002c1"},
+ "p1_acme": {:hex, :p1_acme, "1.0.16", "88b84cc24c9b6eb87204ea53969ccd9b524dcd4142de632441fdd2859ccab778", [:rebar3], [{:base64url, "1.0.1", [hex: :base64url, repo: "hexpm", optional: false]}, {:idna, "6.0.0", [hex: :idna, repo: "hexpm", optional: false]}, {:jiffy, "1.0.5", [hex: :jiffy, repo: "hexpm", optional: false]}, {:jose, "1.11.1", [hex: :jose, repo: "hexpm", optional: false]}, {:yconf, "1.0.12", [hex: :yconf, repo: "hexpm", optional: false]}], "hexpm", "ec0ef380a7345c38b57899733f6fece97c337a3d44fd02cc8898f6a2491a38a8"},
"p1_mysql": {:hex, :p1_mysql, "1.0.19", "22f1be58397780a7d580a954e7af66cde32a29dee1a24ab2aa196272fc654a4a", [:rebar3], [], "hexpm", "88f6cdb510e8959c14b6ae84ccda04967e3de239228f859d8341da67949622b1"},
"p1_oauth2": {:hex, :p1_oauth2, "0.6.10", "09ba1fbd447b1f480b223903e36d0415f21be592a1b00db964eea01285749028", [:rebar3], [], "hexpm", "c79cb61ababee4a8c85409b7f4932035797c093aeef1f9f53985e512b26f2a64"},
"p1_pgsql": {:hex, :p1_pgsql, "1.1.12", "10ae79eeb35ea98c0424a8b6420542fef9e4469eb12ccf41475d10840c291e68", [:rebar3], [], "hexpm", "32203f779e01cf0353270df24833a1d831ad7cb3e3e8e35a7556dfa1f40948d5"},
"p1_utils": {:hex, :p1_utils, "1.0.23", "7f94466ada69bd982ea7bb80fbca18e7053e7d0b82c9d9e37621fa508587069b", [:rebar3], [], "hexpm", "47f21618694eeee5006af1c88731ad86b757161e7823c29b6f73921b571c8502"},
"pkix": {:hex, :pkix, "1.0.8", "98ea05243847fd4504f7c7a0cd82cecd1010ac327a082e1c674c5384006eae75", [:rebar3], [], "hexpm", "399508819501fab9d2e586dfa601b5ee3ef22b5612d3db58204dd2d089ef45d7"},
"stringprep": {:hex, :stringprep, "1.0.27", "02808c7024bc6285ca6a8a67e7addfc16f35dda55551a582c5181d8ea960e890", [:rebar3], [{:p1_utils, "1.0.23", [hex: :p1_utils, repo: "hexpm", optional: false]}], "hexpm", "a5967b1144ca8002a58a03d16dd109fbd0bcdb82616cead2f983944314af6a00"},
- "stun": {:hex, :stun, "1.0.44", "30b6b774864b24b05ba901291abe583bff19081e7c4efb3361df50b781ec9d3b", [:rebar3], [{:fast_tls, "1.1.13", [hex: :fast_tls, repo: "hexpm", optional: false]}, {:p1_utils, "1.0.23", [hex: :p1_utils, repo: "hexpm", optional: false]}], "hexpm", "e45bba816cbefff01d820e49e66814f450df25a7a468a70d68d1e64218d46520"},
+ "stun": {:hex, :stun, "1.0.47", "fae94c0dc7415263297e8f07f286f3355d327d8bf78b1b0743c9a5a492381f71", [:rebar3], [{:fast_tls, "1.1.13", [hex: :fast_tls, repo: "hexpm", optional: false]}, {:p1_utils, "1.0.23", [hex: :p1_utils, repo: "hexpm", optional: false]}], "hexpm", "377d8487f4add85f6bc6ecdebdb4dcbcbe890e9462f27d6d31f3db1cf9b2cc9b"},
"unicode_util_compat": {:hex, :unicode_util_compat, "0.4.1", "d869e4c68901dd9531385bb0c8c40444ebf624e60b6962d95952775cac5e90cd", [:rebar3], [], "hexpm", "1d1848c40487cdb0b30e8ed975e34e025860c02e419cb615d255849f3427439d"},
- "xmpp": {:hex, :xmpp, "1.5.4", "6cd8144b3fe04745dc2cb3e746d6f2a963bb283db48a61f159b49cbe3fab8623", [:rebar3], [{:ezlib, "1.0.10", [hex: :ezlib, repo: "hexpm", optional: false]}, {:fast_tls, "1.1.13", [hex: :fast_tls, repo: "hexpm", optional: false]}, {:fast_xml, "1.1.47", [hex: :fast_xml, repo: "hexpm", optional: false]}, {:idna, "6.0.0", [hex: :idna, repo: "hexpm", optional: false]}, {:p1_utils, "1.0.23", [hex: :p1_utils, repo: "hexpm", optional: false]}, {:stringprep, "1.0.27", [hex: :stringprep, repo: "hexpm", optional: false]}], "hexpm", "3bc2b5cb24e52964fb11641422ce2b7ba7c261dd50080689a1cbe3d952a9db35"},
+ "xmpp": {:hex, :xmpp, "1.5.6", "09259177a39c880d682817932f4da0537c471160fd43aa891ea9cb71cf827b52", [:rebar3], [{:ezlib, "1.0.10", [hex: :ezlib, repo: "hexpm", optional: false]}, {:fast_tls, "1.1.13", [hex: :fast_tls, repo: "hexpm", optional: false]}, {:fast_xml, "1.1.48", [hex: :fast_xml, repo: "hexpm", optional: false]}, {:idna, "6.0.0", [hex: :idna, repo: "hexpm", optional: false]}, {:p1_utils, "1.0.23", [hex: :p1_utils, repo: "hexpm", optional: false]}, {:stringprep, "1.0.27", [hex: :stringprep, repo: "hexpm", optional: false]}], "hexpm", "59b7317c4077d3384f9a891e0517a591cdbd44a323260b835eafbede4f4eb12e"},
"yconf": {:hex, :yconf, "1.0.12", "78c119d39bb805207fcb7671cb884805d75ee89c9ec98632b678f90a597dee2c", [:rebar3], [{:fast_yaml, "1.0.32", [hex: :fast_yaml, repo: "hexpm", optional: false]}], "hexpm", "12faa51c281e95bcb6abf185fd034a242209621a7bb04b6cc411c867b192e207"},
}
diff --git a/priv/msgs/fr.msg b/priv/msgs/fr.msg
index f33d6115d..d35a2fbd5 100644
--- a/priv/msgs/fr.msg
+++ b/priv/msgs/fr.msg
@@ -5,11 +5,20 @@
{" (Add * to the end of field to match substring)"," (Ajouter * à la fin du champ pour correspondre à la sous-chaîne)"}.
{" has set the subject to: "," a défini le sujet sur : "}.
+{"# participants","# participants"}.
{"A description of the node","Une description du nœud"}.
{"A friendly name for the node","Un nom convivial pour le nœud"}.
{"A password is required to enter this room","Un mot de passe est nécessaire pour accéder à ce salon"}.
+{"A Web Page","Une page Web"}.
{"Accept","Accepter"}.
{"Access denied by service policy","L'accès au service est refusé"}.
+{"Access model of authorize","Modèle d’accès de « autoriser »"}.
+{"Access model of open","Modèle d’accès de « ouvrir »"}.
+{"Access model of presence","Modèle d’accès de « présence »"}.
+{"Access model of roster","Modèle d’accès de « liste »"}.
+{"Access model of whitelist","Modèle d’accès de « liste blanche »"}.
+{"Access model","Modèle d’accès"}.
+{"Account doesn't exist","Le compte n’existe pas"}.
{"Action on user","Action sur l'utilisateur"}.
{"Add Jabber ID","Ajouter un Jabber ID"}.
{"Add New","Ajouter"}.
@@ -19,7 +28,9 @@
{"Administrator privileges required","Les droits d'administrateur sont nécessaires"}.
{"All activity","Toute activité"}.
{"All Users","Tous les utilisateurs"}.
+{"Allow subscription","Autoriser l’abonnement"}.
{"Allow this Jabber ID to subscribe to this pubsub node?","Autoriser ce Jabber ID à s'abonner à ce nœud PubSub ?"}.
+{"Allow this person to register with the room?","Autoriser cette personne à enregistrer ce salon ?"}.
{"Allow users to change the subject","Autoriser les utilisateurs à changer le sujet"}.
{"Allow users to query other users","Autoriser les utilisateurs à envoyer des requêtes aux autres utilisateurs"}.
{"Allow users to send invites","Autoriser les utilisateurs à envoyer des invitations"}.
@@ -28,8 +39,25 @@
{"Allow visitors to send private messages to","Autoriser les visiteurs à envoyer des messages privés"}.
{"Allow visitors to send status text in presence updates","Autoriser les visiteurs à envoyer un message d'état avec leur présence"}.
{"Allow visitors to send voice requests","Permettre aux visiteurs d'envoyer des demandes de 'voice'"}.
+{"An associated LDAP group that defines room membership; this should be an LDAP Distinguished Name according to an implementation-specific or deployment-specific definition of a group.","Un groupe LDAP associé qui définit l’adhésion à un salon ; cela devrait être un nom distingué LDAP selon la définition spécifique à l’implémentation ou au déploiement d’un groupe."}.
{"Announcements","Annonces"}.
+{"Answer associated with a picture","Réponse associée à une image"}.
+{"Answer associated with a video","Réponse associée à une vidéo"}.
+{"Answer associated with speech","Réponse associée à un discours"}.
+{"Answer to a question","Réponse à une question"}.
+{"Anyone in the specified roster group(s) may subscribe and retrieve items","N’importe qui dans le groupe de la liste spécifiée peut s’abonner et récupérer les items"}.
+{"Anyone may associate leaf nodes with the collection","N’importe qui peut associer les feuilles avec la collection"}.
+{"Anyone may publish","N’importe qui peut publier"}.
+{"Anyone may subscribe and retrieve items","N’importe qui peut s’abonner et récupérer les items"}.
+{"Anyone with a presence subscription of both or from may subscribe and retrieve items","N’importe qui avec un abonnement de présence peut s’abonner et récupérer les items"}.
+{"Anyone with Voice","N’importe qui avec Voice"}.
+{"Anyone","N’importe qui"}.
{"April","Avril"}.
+{"Attribute 'channel' is required for this request","L’attribut « channel » est requis pour la requête"}.
+{"Attribute 'id' is mandatory for MIX messages","L’attribut « id » est obligatoire pour les messages MIX"}.
+{"Attribute 'jid' is not allowed here","L’attribut « jid » n’est pas autorisé ici"}.
+{"Attribute 'node' is not allowed here","L’attribut « node » n’est pas autorisé ici"}.
+{"Attribute 'to' of stanza that triggered challenge","L’attribut « to » de la strophe qui a déclenché le défi"}.
{"August","Août"}.
{"Automatic node creation is not enabled","La creation implicite de nœud n'est pas disponible"}.
{"Backup Management","Gestion des sauvegardes"}.
@@ -43,10 +71,14 @@
{"Cannot remove active list","La liste active ne peut être supprimée"}.
{"Cannot remove default list","La liste par défaut ne peut être supprimée"}.
{"CAPTCHA web page","Page web de CAPTCHA"}.
+{"Challenge ID","Identifiant du défi"}.
{"Change Password","Modifier le mot de passe"}.
{"Change User Password","Changer le mot de passe de l'utilisateur"}.
{"Changing password is not allowed","La modification du mot de passe n'est pas autorisée"}.
{"Changing role/affiliation is not allowed","La modification role/affiliation n'est pas autorisée"}.
+{"Channel already exists","Ce canal existe déjà"}.
+{"Channel does not exist","Le canal n’existe pas"}.
+{"Channels","Canaux"}.
{"Characters not allowed:","Caractères non autorisés :"}.
{"Chatroom configuration modified","Configuration du salon modifiée"}.
{"Chatroom is created","Le salon de discussion est créé"}.
@@ -58,30 +90,39 @@
{"Choose storage type of tables","Choisissez un type de stockage pour les tables"}.
{"Choose whether to approve this entity's subscription.","Choisissez d'approuver ou non l'abonnement de cette entité."}.
{"City","Ville"}.
+{"Client acknowledged more stanzas than sent by server","Le client accuse réception de plus de strophes que ce qui est envoyé par le serveur"}.
{"Commands","Commandes"}.
{"Conference room does not exist","Le salon de discussion n'existe pas"}.
{"Configuration of room ~s","Configuration pour le salon ~s"}.
{"Configuration","Configuration"}.
{"Connected Resources:","Ressources connectées :"}.
+{"Contact Addresses (normally, room owner or owners)","Adresses de contact (normalement les administrateurs du salon)"}.
{"Country","Pays"}.
{"CPU Time:","Temps CPU :"}.
+{"Current Discussion Topic","Sujet de discussion courant"}.
{"Database failure","Échec sur la base de données"}.
{"Database Tables at ~p","Tables de base de données sur ~p"}.
{"Database Tables Configuration at ","Configuration des tables de base de données sur "}.
{"Database","Base de données"}.
{"December","Décembre"}.
{"Default users as participants","Les utilisateurs sont participant par défaut"}.
+{"Delete content","Supprimer le contenu"}.
{"Delete message of the day on all hosts","Supprimer le message du jour sur tous les domaines"}.
{"Delete message of the day","Supprimer le message du jour"}.
{"Delete Selected","Suppression des éléments sélectionnés"}.
+{"Delete table","Supprimer la table"}.
{"Delete User","Supprimer l'utilisateur"}.
{"Deliver event notifications","Envoyer les notifications d'événement"}.
{"Deliver payloads with event notifications","Inclure le contenu du message avec la notification"}.
{"Description:","Description :"}.
{"Disc only copy","Copie sur disque uniquement"}.
+{"'Displayed groups' not added (they do not exist!): ","« Groupes affichés » non ajoutés (ils n’existent pas !) : "}.
{"Displayed:","Affichés :"}.
+{"Don't tell your password to anybody, not even the administrators of the XMPP server.","Ne révélez votre mot de passe à personne, pas même aux administrateurs du serveur XMPP."}.
{"Dump Backup to Text File at ","Enregistrer la sauvegarde dans un fichier texte sur "}.
{"Dump to Text File","Sauvegarder dans un fichier texte"}.
+{"Duplicated groups are not allowed by RFC6121","Les groupes dupliqués ne sont pas autorisés par la RFC6121"}.
+{"Dynamically specify a replyto of the item publisher","Spécifie dynamiquement un « réponse à » de l’item de l’éditeur"}.
{"Edit Properties","Modifier les propriétés"}.
{"Either approve or decline the voice request.","Accepter ou refuser la demande de voix."}.
{"ejabberd MUC module","Module MUC ejabberd"}.
@@ -116,6 +157,7 @@
{"Failed to parse HTTP response","Échec de lecture de la réponse HTTP"}.
{"Failed to process option '~s'","Échec de traitement de l'option '~s'"}.
{"Family Name","Nom de famille"}.
+{"FAQ Entry","Entrée FAQ"}.
{"February","Février"}.
{"File larger than ~w bytes","Taille de fichier suppérieur à ~w octets"}.
{"Fill in the form to search for any matching XMPP User","Complétez le formulaire pour rechercher un utilisateur XMPP correspondant"}.
@@ -176,6 +218,7 @@
{"July","Juillet"}.
{"June","Juin"}.
{"Just created","Vient d'être créé"}.
+{"Label:","Étiquette :"}.
{"Last Activity","Dernière activité"}.
{"Last login","Dernière connexion"}.
{"Last month","Dernier mois"}.
@@ -192,7 +235,6 @@
{"Make room public searchable","Rendre le salon public"}.
{"Malformed username","Nom d'utilisateur invalide"}.
{"March","Mars"}.
-{"Max # of items to persist","Nombre maximum d'éléments à stocker"}.
{"Max payload size in bytes","Taille maximum pour le contenu du message en octet"}.
{"Maximum file size","Taille maximale du fichier"}.
{"Maximum Number of History Messages Returned by Room","Nombre maximal de messages d'historique renvoyés par salle"}.
@@ -274,6 +316,7 @@
{"Online Users:","Utilisateurs connectés :"}.
{"Online Users","Utilisateurs en ligne"}.
{"Online","En ligne"}.
+{"Only admins can see this","Seuls les administrateurs peuvent voir cela"}.
{"Only deliver notifications to available users","Envoyer les notifications uniquement aux utilisateurs disponibles"}.
{"Only <enable/> or <disable/> tags are allowed","Seul le tag <enable/> ou <disable/> est autorisé"}.
{"Only <list/> element is allowed in this query","Seul l'élément <list/> est autorisé dans cette requête"}.
@@ -283,6 +326,7 @@
{"Only moderators can approve voice requests","Seuls les modérateurs peuvent accépter les requêtes voix"}.
{"Only occupants are allowed to send messages to the conference","Seuls les occupants peuvent envoyer des messages à la conférence"}.
{"Only occupants are allowed to send queries to the conference","Seuls les occupants sont autorisés à envoyer des requêtes à la conférence"}.
+{"Only publishers may publish","Seuls les éditeurs peuvent publier"}.
{"Only service administrators are allowed to send service messages","Seuls les administrateurs du service sont autoriser à envoyer des messages de service"}.
{"Organization Name","Nom de l'organisation"}.
{"Organization Unit","Unité de l'organisation"}.
@@ -290,6 +334,7 @@
{"Outgoing s2s Connections:","Connexions s2s sortantes :"}.
{"Owner privileges required","Les droits de propriétaire sont nécessaires"}.
{"Packet","Paquet"}.
+{"Participant","Participant"}.
{"Password Verification","Vérification du mot de passe"}.
{"Password Verification:","Vérification du mot de passe :"}.
{"Password","Mot de passe"}.
@@ -323,6 +368,9 @@
{"Remove User","Supprimer l'utilisateur"}.
{"Remove","Supprimer"}.
{"Replaced by new connection","Remplacé par une nouvelle connexion"}.
+{"Request has timed out","La demande a expiré"}.
+{"Request is ignored","La demande est ignorée"}.
+{"Requested role","Rôle demandé"}.
{"Resources","Ressources"}.
{"Restart Service","Redémarrer le service"}.
{"Restart","Redémarrer"}.
diff --git a/priv/msgs/zh.msg b/priv/msgs/zh.msg
index 8f099b608..a3a400afb 100644
--- a/priv/msgs/zh.msg
+++ b/priv/msgs/zh.msg
@@ -4,7 +4,7 @@
%% https://docs.ejabberd.im/developer/extending-ejabberd/localization/
{" (Add * to the end of field to match substring)"," (在字段末添加*来匹配子串)"}.
-{" has set the subject to: ","已将标题设置为: "}.
+{" has set the subject to: "," 已将标题设置为: "}.
{"# participants","# 个参与人"}.
{"A description of the node","该节点的描述"}.
{"A friendly name for the node","该节点的友好名称"}.
@@ -23,7 +23,7 @@
{"Add Jabber ID","添加Jabber ID"}.
{"Add New","添加新用户"}.
{"Add User","添加用户"}.
-{"Administration of ","管理"}.
+{"Administration of ","管理 "}.
{"Administration","管理"}.
{"Administrator privileges required","需要管理员权限"}.
{"All activity","所有活动"}.
@@ -62,7 +62,7 @@
{"Automatic node creation is not enabled","未启用自动节点创建"}.
{"Backup Management","备份管理"}.
{"Backup of ~p","~p的备份"}.
-{"Backup to File at ","备份文件位于"}.
+{"Backup to File at ","备份文件位于 "}.
{"Backup","备份"}.
{"Bad format","格式错误"}.
{"Birthday","出生日期"}.
@@ -102,7 +102,7 @@
{"Current Discussion Topic","当前讨论话题"}.
{"Database failure","数据库失败"}.
{"Database Tables at ~p","位于~p的数据库表"}.
-{"Database Tables Configuration at ","数据库表格配置位于"}.
+{"Database Tables Configuration at ","数据库表格配置位于 "}.
{"Database","数据库"}.
{"December","十二月"}.
{"Default users as participants","用户默认被视为参与人"}.
@@ -119,12 +119,12 @@
{"'Displayed groups' not added (they do not exist!): ","'显示的群组' 未被添加 (它们不存在!): "}.
{"Displayed:","已显示:"}.
{"Don't tell your password to anybody, not even the administrators of the XMPP server.","不要将密码告诉任何人, 就算是XMPP服务器的管理员也不可以."}.
-{"Dump Backup to Text File at ","转储备份到文本文件于"}.
+{"Dump Backup to Text File at ","将备份转储到位于以下位置的文本文件 "}.
{"Dump to Text File","转储到文本文件"}.
{"Duplicated groups are not allowed by RFC6121","按照RFC6121的规则,不允许有重复的群组"}.
{"Dynamically specify a replyto of the item publisher","为项目发布者动态指定一个 replyto"}.
{"Edit Properties","编辑属性"}.
-{"Either approve or decline the voice request.","接受或拒绝声音请求"}.
+{"Either approve or decline the voice request.","接受或拒绝声音请求."}.
{"ejabberd HTTP Upload service","ejabberd HTTP 上传服务"}.
{"ejabberd MUC module","ejabberd MUC 模块"}.
{"ejabberd Multicast service","ejabberd多重映射服务"}.
@@ -194,10 +194,10 @@
{"Import Directory","导入目录"}.
{"Import File","导入文件"}.
{"Import user data from jabberd14 spool file:","从 jabberd14 Spool 文件导入用户数据:"}.
-{"Import User from File at ","导入用户的文件位于"}.
+{"Import User from File at ","从以下位置的文件导入用户 "}.
{"Import users data from a PIEFXIS file (XEP-0227):","从 PIEFXIS 文件 (XEP-0227) 导入用户数据:"}.
{"Import users data from jabberd14 spool directory:","从jabberd14 Spool目录导入用户数据:"}.
-{"Import Users from Dir at ","导入用户的目录位于"}.
+{"Import Users from Dir at ","从以下位置目录导入用户 "}.
{"Import Users From jabberd14 Spool Files","从 jabberd14 Spool 文件导入用户"}.
{"Improper domain part of 'from' attribute","不恰当的'from'属性域名部分"}.
{"Improper message type","不恰当的消息类型"}.
@@ -249,7 +249,6 @@
{"Malformed username","用户名无效"}.
{"MAM preference modification denied by service policy","MAM偏好被服务策略拒绝"}.
{"March","三月"}.
-{"Max # of items to persist","允许持久化的最大内容条目数"}.
{"Max payload size in bytes","最大有效负载字节数"}.
{"Maximum file size","最大文件大小"}.
{"Maximum Number of History Messages Returned by Room","房间返回的历史消息最大值"}.
@@ -288,7 +287,7 @@
{"Never","从未"}.
{"New Password:","新密码:"}.
{"Nickname can't be empty","昵称不能为空"}.
-{"Nickname Registration at ","昵称注册于"}.
+{"Nickname Registration at ","昵称注册于 "}.
{"Nickname ~s does not exist in the room","昵称~s不在该房间"}.
{"Nickname","昵称"}.
{"No address elements found","没有找到地址的各元素"}.
@@ -326,6 +325,7 @@
{"Node ~p","节点~p"}.
{"Nodeprep has failed","Nodeprep 已失效"}.
{"Nodes","节点"}.
+{"Node","节点"}.
{"None","无"}.
{"Not allowed","不允许"}.
{"Not Found","没有找到"}.
@@ -339,7 +339,6 @@
{"Number of Offline Messages","离线消息数量"}.
{"Number of online users","在线用户数"}.
{"Number of registered users","注册用户数"}.
-{"Number of seconds after which to automatically purge items","自动清除项目要等待的秒数"}.
{"Occupants are allowed to invite others","允许成员邀请其他人"}.
{"Occupants May Change the Subject","成员可以修改主题"}.
{"October","十月"}.
@@ -428,7 +427,7 @@
{"Resources","资源"}.
{"Restart Service","重启服务"}.
{"Restart","重启"}.
-{"Restore Backup from File at ","要恢复的备份文件位于"}.
+{"Restore Backup from File at ","从以下位置的文件恢复备份 "}.
{"Restore binary backup after next ejabberd restart (requires less memory):","在下次 ejabberd 重启后恢复二进制备份(需要的内存更少):"}.
{"Restore binary backup immediately:","立即恢复二进制备份:"}.
{"Restore plain text backup immediately:","立即恢复普通文本备份:"}.
@@ -455,7 +454,7 @@
{"Search Results for ","搜索结果属于关键词 "}.
{"Search the text","搜索文本"}.
{"Search until the date","搜索截至日期"}.
-{"Search users in ","搜索用户于"}.
+{"Search users in ","在以下位置搜索用户 "}.
{"Select All","全选"}.
{"Send announcement to all online users on all hosts","发送通知给所有主机的在线用户"}.
{"Send announcement to all online users","发送通知给所有在线用户"}.
@@ -519,7 +518,6 @@
{"The JIDs of those with an affiliation of owner","隶属所有人的JID"}.
{"The JIDs of those with an affiliation of publisher","隶属发布人的JID"}.
{"The list of JIDs that may associate leaf nodes with a collection","可以将叶节点与集合关联的JID列表"}.
-{"The maximum number of child nodes that can be associated with a collection","可以与集合关联的最大子节点数"}.
{"The minimum number of milliseconds between sending any two notification digests","发送任何两个通知摘要之间的最小毫秒数"}.
{"The name of the node","该节点的名称"}.
{"The node is a collection node","该节点是集合节点"}.
@@ -544,13 +542,12 @@
{"The type of node data, usually specified by the namespace of the payload (if any)","节点数据的类型, 如果有, 通常由有效负载的名称空间指定"}.
{"The URL of an XSL transformation which can be applied to payloads in order to generate an appropriate message body element.","XSL转换的URL,可以将其应用于有效负载以生成适当的消息正文元素。"}.
{"The URL of an XSL transformation which can be applied to the payload format in order to generate a valid Data Forms result that the client could display using a generic Data Forms rendering engine","XSL转换的URL, 可以将其应用于有效负载格式, 以生成有效的数据表单结果, 客户端可以使用通用数据表单呈现引擎来显示该结果"}.
-{"The username is not valid","用户名无效"}.
{"There was an error changing the password: ","修改密码出错: "}.
{"There was an error creating the account: ","帐户创建出错: "}.
{"There was an error deleting the account: ","帐户删除失败: "}.
{"This is case insensitive: macbeth is the same that MacBeth and Macbeth.","此处不区分大小写: macbeth 与 MacBeth 和 Macbeth 是一样的."}.
{"This page allows to register an XMPP account in this XMPP server. Your JID (Jabber ID) will be of the form: username@server. Please read carefully the instructions to fill correctly the fields.","本页面允许在此服务器上注册XMPP帐户. 你的JID (Jabber ID) 的形式如下: 用户名@服务器. 请仔细阅读说明并正确填写相应字段."}.
-{"This page allows to unregister an XMPP account in this XMPP server.","此页面允许在此XMPP服务器上注销XMPP帐户"}.
+{"This page allows to unregister an XMPP account in this XMPP server.","此页面允许在此 XMPP 服务器上注销 XMPP 帐户。"}.
{"This room is not anonymous","此房间不是匿名房间"}.
{"This service can not process the address: ~s","此服务无法处理地址: ~s"}.
{"Thursday","星期四"}.
@@ -565,7 +562,7 @@
{"Too many child elements","太多子元素"}.
{"Too many <item/> elements","太多 <item/> 元素"}.
{"Too many <list/> elements","太多 <list/> 元素"}.
-{"Too many (~p) failed authentications from this IP address (~s). The address will be unblocked at ~s UTC","来自IP地址(~p)的(~s)失败认证太多. 该地址将在UTC时间~s被禁用."}.
+{"Too many (~p) failed authentications from this IP address (~s). The address will be unblocked at ~s UTC","来自IP地址(~p)的(~s)失败认证太多。将在UTC时间 ~s 解除对该地址的封锁"}.
{"Too many receiver fields were specified","指定的接收者字段太多"}.
{"Too many unacked stanzas","未被确认的节太多"}.
{"Too many users in this conference","该会议的用户太多"}.
@@ -656,7 +653,7 @@
{"You need a client that supports x:data to register the nickname","您需要一个支持 x:data 的客户端来注册昵称"}.
{"You need an x:data capable client to search","您需要一个兼容 x:data 的客户端来搜索"}.
{"Your active privacy list has denied the routing of this stanza.","你的活跃私聊列表拒绝了在此房间进行路由分发."}.
-{"Your contact offline message queue is full. The message has been discarded.","您的联系人离线消息队列已满. 消息已被丢弃"}.
+{"Your contact offline message queue is full. The message has been discarded.","您的联系人离线消息队列已满。消息已被丢弃。"}.
{"Your subscription request and/or messages to ~s have been blocked. To unblock your subscription request, visit ~s","您发送给~s的消息已被阻止. 要解除阻止, 请访问 ~s"}.
{"Your XMPP account was successfully registered.","你的XMPP帐户注册成功."}.
{"Your XMPP account was successfully unregistered.","你的XMPP帐户注销成功."}.
diff --git a/rebar.config b/rebar.config
index a70ccfba3..0c18d9e7d 100644
--- a/rebar.config
+++ b/rebar.config
@@ -30,25 +30,31 @@
{if_var_true, redis,
{eredis, ".*", {git, "https://github.com/wooga/eredis", {tag, "v1.2.0"}}}},
{if_var_true, sip,
- {esip, ".*", {git, "https://github.com/processone/esip", {tag, "1.0.43"}}}},
+ {esip, ".*", {git, "https://github.com/processone/esip", {tag, "1.0.45"}}}},
{if_var_true, zlib,
{ezlib, ".*", {git, "https://github.com/processone/ezlib", {tag, "1.0.10"}}}},
{fast_tls, ".*", {git, "https://github.com/processone/fast_tls", {tag, "1.1.13"}}},
- {fast_xml, ".*", {git, "https://github.com/processone/fast_xml", {tag, "1.1.47"}}},
+ {fast_xml, ".*", {git, "https://github.com/processone/fast_xml", {tag, "1.1.48"}}},
{fast_yaml, ".*", {git, "https://github.com/processone/fast_yaml", {tag, "1.0.32"}}},
{idna, ".*", {git, "https://github.com/benoitc/erlang-idna", {tag, "6.0.0"}}},
{jiffy, ".*", {git, "https://github.com/davisp/jiffy", {tag, "1.0.5"}}},
- {jose, ".*", {git, "https://github.com/potatosalad/erlang-jose", {tag, "1.9.0"}}},
+ {jose, ".*", {git, "https://github.com/potatosalad/erlang-jose", {tag, "1.11.1"}}},
{lager, ".*", {git, "https://github.com/erlang-lager/lager", {tag, "3.9.1"}}},
{if_var_true, lua,
- {luerl, ".*", {git, "https://github.com/rvirding/luerl", {tag, "v0.3"}}}},
+ {if_not_rebar3,
+ {luerl, ".*", {git, "https://github.com/rvirding/luerl", {tag, "1.0"}}}
+ }},
+ {if_var_true, lua,
+ {if_rebar3,
+ {luerl, ".*", {git, "https://github.com/rvirding/luerl", {tag, "1.0.0"}}}
+ }},
{mqtree, ".*", {git, "https://github.com/processone/mqtree", {tag, "1.0.14"}}},
- {p1_acme, ".*", {git, "https://github.com/processone/p1_acme", {tag, "1.0.13"}}},
+ {p1_acme, ".*", {git, "https://github.com/processone/p1_acme", {tag, "1.0.16"}}},
{if_var_true, mysql,
{p1_mysql, ".*", {git, "https://github.com/processone/p1_mysql", {tag, "1.0.19"}}}},
{p1_oauth2, ".*", {git, "https://github.com/processone/p1_oauth2", {tag, "0.6.10"}}},
{if_var_true, pgsql,
- {p1_pgsql, ".*", {git, "https://github.com/processone/p1_pgsql", {tag, "1.1.12"}}}},
+ {p1_pgsql, ".*", {git, "https://github.com/processone/p1_pgsql", {tag, "1.1.16"}}}},
{p1_utils, ".*", {git, "https://github.com/processone/p1_utils", {tag, "1.0.23"}}},
{pkix, ".*", {git, "https://github.com/processone/pkix", {tag, "1.0.8"}}},
{if_not_rebar3, %% Needed because modules are not fully migrated to new structure and mix
@@ -58,12 +64,12 @@
{sqlite3, ".*", {git, "https://github.com/processone/erlang-sqlite3", {tag, "1.1.13"}}}},
{stringprep, ".*", {git, "https://github.com/processone/stringprep", {tag, "1.0.27"}}},
{if_var_true, stun,
- {stun, ".*", {git, "https://github.com/processone/stun", {tag, "1.0.44"}}}},
- {xmpp, ".*", {git, "https://github.com/processone/xmpp", "e943c0285aa85e3cbd4bfb9259f6b7de32b00395"}},
+ {stun, ".*", {git, "https://github.com/processone/stun", {tag, "1.0.47"}}}},
+ {xmpp, ".*", {git, "https://github.com/processone/xmpp", {tag, "1.5.6"}}},
{yconf, ".*", {git, "https://github.com/processone/yconf", {tag, "1.0.12"}}}
]}.
-{gitonly_deps, [elixir, luerl]}.
+{gitonly_deps, [elixir]}.
{if_var_true, latest_deps,
{floating_deps, [cache_tab,
@@ -110,6 +116,7 @@
{if_var_true, sip, {d, 'SIP'}},
{if_var_true, stun, {d, 'STUN'}},
{if_have_fun, {erl_error, format_exception, 6}, {d, 'HAVE_ERL_ERROR'}},
+ {if_have_fun, {uri_string, normalize, 1}, {d, 'HAVE_URI_STRING'}},
{src_dirs, [src,
{if_rebar3, sql},
{if_var_true, tools, tools},
@@ -177,12 +184,10 @@
{sys_config, "./rel/sys.config"},
{vm_args, "./rel/vm.args"},
{overlay_vars, "vars.config"},
- {extended_start_script, true},
{overlay, [{mkdir, "var/log/ejabberd"},
- {mkdir, "var/lock"},
{mkdir, "var/lib/ejabberd"},
{mkdir, "etc/ejabberd"},
- {copy, "rel/files/erl", "\{\{erts_vsn\}\}/bin/erl"}, % in rebar2 this prepends erts-
+ {copy, "rel/files/erl", "erts-\{\{erts_vsn\}\}/bin/erl"},
{template, "ejabberdctl.template", "bin/ejabberdctl"},
{copy, "inetrc", "etc/ejabberd/inetrc"},
{copy, "tools/captcha*.sh", "lib/ejabberd-\{\{release_version\}\}/priv/bin/"},
@@ -193,6 +198,7 @@
{dev_mode, false},
{include_erts, true},
{include_src, true},
+ {generate_start_script, false},
{overlay, [{copy, "sql/*", "lib/ejabberd-\{\{release_version\}\}/priv/sql/"},
{copy, "ejabberdctl.cfg.example", "etc/ejabberd/ejabberdctl.cfg"},
{copy, "ejabberd.yml.example", "etc/ejabberd/ejabberd.yml"}]}]}]},
@@ -201,6 +207,8 @@
{dev_mode, true},
{include_erts, true},
{include_src, false},
+ {generate_start_script, true},
+ {extended_start_script, true},
{overlay, [{copy, "ejabberdctl.cfg.example", "etc/ejabberd/ejabberdctl.cfg.example"},
{copy, "ejabberd.yml.example", "etc/ejabberd/ejabberd.yml.example"},
{copy, "test/ejabberd_SUITE_data/ca.pem", "etc/ejabberd/"},
diff --git a/rebar.config.script b/rebar.config.script
index efd51d6ba..c83390fb5 100644
--- a/rebar.config.script
+++ b/rebar.config.script
@@ -387,8 +387,8 @@ Rules = [
]}]), []},
{[plugins], IsRebar3 and (os:getenv("GITHUB_ACTIONS") == "true"),
AppendList([{coveralls, {git,
- "https://github.com/RoadRunnr/coveralls-erl.git",
- {branch, "feature/git-info"}}} ]), []},
+ "https://github.com/processone/coveralls-erl.git",
+ {branch, "addjsonfile"}}} ]), []},
{[overrides], [post_hook_configure], SystemDeps == false,
AppendList2(GenDepsConfigure), [], []},
{[ct_extra_params], [eunit_compile_opts], true,
diff --git a/rel/reltool.config.script b/rel/reltool.config.script
index 459077964..951d55d28 100644
--- a/rel/reltool.config.script
+++ b/rel/reltool.config.script
@@ -89,7 +89,6 @@ Sys = [{lib_dirs, []},
Overlay = [
{mkdir, "var/log/ejabberd"},
- {mkdir, "var/lock"},
{mkdir, "var/lib/ejabberd"},
{mkdir, "etc/ejabberd"},
{mkdir, "doc"},
diff --git a/sql/lite.new.sql b/sql/lite.new.sql
index 96c880358..cb8add8a1 100644
--- a/sql/lite.new.sql
+++ b/sql/lite.new.sql
@@ -330,6 +330,7 @@ CREATE TABLE muc_room_subscribers (
);
CREATE INDEX i_muc_room_subscribers_host_jid ON muc_room_subscribers(host, jid);
+CREATE INDEX i_muc_room_subscribers_jid ON muc_room_subscribers(jid);
CREATE UNIQUE INDEX i_muc_room_subscribers_host_room_jid ON muc_room_subscribers(host, room, jid);
CREATE TABLE motd (
diff --git a/sql/lite.sql b/sql/lite.sql
index 087035d7f..56e8c9151 100644
--- a/sql/lite.sql
+++ b/sql/lite.sql
@@ -302,6 +302,7 @@ CREATE TABLE muc_room_subscribers (
);
CREATE INDEX i_muc_room_subscribers_host_jid ON muc_room_subscribers(host, jid);
+CREATE INDEX i_muc_room_subscribers_jid ON muc_room_subscribers(jid);
CREATE UNIQUE INDEX i_muc_room_subscribers_host_room_jid ON muc_room_subscribers(host, room, jid);
CREATE TABLE motd (
diff --git a/sql/mssql.sql b/sql/mssql.sql
index bb7861527..f8b65edc4 100644
--- a/sql/mssql.sql
+++ b/sql/mssql.sql
@@ -150,6 +150,7 @@ CREATE TABLE [dbo].[muc_room_subscribers] (
CREATE UNIQUE CLUSTERED INDEX [muc_room_subscribers_host_room_jid] ON [muc_room_subscribers] (host, room, jid);
CREATE INDEX [muc_room_subscribers_host_jid] ON [muc_room_subscribers] (host, jid);
+CREATE INDEX [muc_room_subscribers_jid] ON [muc_room_subscribers] (jid);
CREATE TABLE [dbo].[privacy_default_list] (
[username] [varchar] (250) NOT NULL,
diff --git a/sql/mysql.new.sql b/sql/mysql.new.sql
index 01aeffbc5..c4e021abf 100644
--- a/sql/mysql.new.sql
+++ b/sql/mysql.new.sql
@@ -347,6 +347,7 @@ CREATE TABLE muc_room_subscribers (
) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE INDEX i_muc_room_subscribers_host_jid USING BTREE ON muc_room_subscribers(host, jid);
+CREATE INDEX i_muc_room_subscribers_jid USING BTREE ON muc_room_subscribers(jid);
CREATE TABLE motd (
username varchar(191) NOT NULL,
diff --git a/sql/mysql.old-to-new.sql b/sql/mysql.old-to-new.sql
index 59c9befe2..9614d55a8 100644
--- a/sql/mysql.old-to-new.sql
+++ b/sql/mysql.old-to-new.sql
@@ -77,6 +77,7 @@ BEGIN
ALTER TABLE `last` ADD PRIMARY KEY (`server_host`, `username`);
ALTER TABLE `sr_group` ADD COLUMN `server_host` VARCHAR (191) COLLATE `utf8mb4_unicode_ci` NOT NULL DEFAULT @DEFAULT_HOST AFTER `name`;
ALTER TABLE `sr_group` ALTER COLUMN `server_host` DROP DEFAULT;
+ ALTER TABLE `sr_group` ADD UNIQUE INDEX `i_sr_group_sh_name` (`server_host`, `name`);
ALTER TABLE `sr_group` ADD PRIMARY KEY (`server_host`, `name`);
ALTER TABLE `muc_registered` ADD COLUMN `server_host` VARCHAR (191) COLLATE `utf8mb4_unicode_ci` NOT NULL DEFAULT @DEFAULT_HOST AFTER `host`;
ALTER TABLE `muc_registered` ALTER COLUMN `server_host` DROP DEFAULT;
@@ -99,6 +100,7 @@ BEGIN
ALTER TABLE `sr_user` DROP INDEX `i_sr_user_jid_group`;
ALTER TABLE `sr_user` ADD COLUMN `server_host` VARCHAR (191) COLLATE `utf8mb4_unicode_ci` NOT NULL DEFAULT @DEFAULT_HOST AFTER `jid`;
ALTER TABLE `sr_user` ALTER COLUMN `server_host` DROP DEFAULT;
+ ALTER TABLE `sr_user` ADD UNIQUE INDEX `i_sr_user_sh_jid_group` (`server_host`, `jid`, `grp`);
ALTER TABLE `sr_user` ADD INDEX `i_sr_user_sh_jid` (`server_host`, `jid`);
ALTER TABLE `sr_user` ADD INDEX `i_sr_user_sh_grp` (`server_host`, `grp`);
ALTER TABLE `sr_user` ADD PRIMARY KEY (`server_host`, `jid`, `grp`);
diff --git a/sql/mysql.sql b/sql/mysql.sql
index 7feaf5d0a..751552e19 100644
--- a/sql/mysql.sql
+++ b/sql/mysql.sql
@@ -319,6 +319,7 @@ CREATE TABLE muc_room_subscribers (
) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE INDEX i_muc_room_subscribers_host_jid USING BTREE ON muc_room_subscribers(host, jid);
+CREATE INDEX i_muc_room_subscribers_jid USING BTREE ON muc_room_subscribers(jid);
CREATE TABLE motd (
username varchar(191) PRIMARY KEY,
diff --git a/sql/pg.new.sql b/sql/pg.new.sql
index b3473a1a0..5ffbedde3 100644
--- a/sql/pg.new.sql
+++ b/sql/pg.new.sql
@@ -311,7 +311,7 @@ CREATE TABLE vcard_search (
lorgname text NOT NULL,
orgunit text NOT NULL,
lorgunit text NOT NULL,
- PRIMARY KEY (server_host, username)
+ PRIMARY KEY (server_host, lusername)
);
CREATE INDEX i_vcard_search_sh_lfn ON vcard_search(server_host, lfn);
@@ -495,6 +495,7 @@ CREATE TABLE muc_room_subscribers (
);
CREATE INDEX i_muc_room_subscribers_host_jid ON muc_room_subscribers USING btree (host, jid);
+CREATE INDEX i_muc_room_subscribers_jid ON muc_room_subscribers USING btree (jid);
CREATE UNIQUE INDEX i_muc_room_subscribers_host_room_jid ON muc_room_subscribers USING btree (host, room, jid);
CREATE TABLE motd (
diff --git a/sql/pg.sql b/sql/pg.sql
index 0e3d4c8b8..733856ede 100644
--- a/sql/pg.sql
+++ b/sql/pg.sql
@@ -320,6 +320,7 @@ CREATE TABLE muc_room_subscribers (
);
CREATE INDEX i_muc_room_subscribers_host_jid ON muc_room_subscribers USING btree (host, jid);
+CREATE INDEX i_muc_room_subscribers_jid ON muc_room_subscribers USING btree (jid);
CREATE UNIQUE INDEX i_muc_room_subscribers_host_room_jid ON muc_room_subscribers USING btree (host, room, jid);
CREATE TABLE motd (
diff --git a/src/ejabberd_admin.erl b/src/ejabberd_admin.erl
index 0174cd7ff..9e72c7b36 100644
--- a/src/ejabberd_admin.erl
+++ b/src/ejabberd_admin.erl
@@ -120,7 +120,10 @@ get_commands_spec() ->
module = init, function = restart,
args = [], result = {res, rescode}},
#ejabberd_commands{name = reopen_log, tags = [logs],
- desc = "Reopen the log files",
+ desc = "Reopen the log files after being renamed",
+ longdesc = "This can be useful when an external tool is "
+ "used for log rotation. See "
+ "https://docs.ejabberd.im/admin/guide/troubleshooting/#log-files",
policy = admin,
module = ?MODULE, function = reopen_log,
args = [], result = {res, rescode}},
@@ -178,6 +181,8 @@ get_commands_spec() ->
result = {res, restuple}},
#ejabberd_commands{name = unregister, tags = [accounts],
desc = "Unregister a user",
+ longdesc = "This deletes the authentication and all the "
+ "data associated to the account (roster, vcard...).",
policy = admin,
module = ?MODULE, function = unregister,
args_desc = ["Username", "Local vhost served by ejabberd"],
@@ -345,31 +350,41 @@ get_commands_spec() ->
{oldbackup, string}, {newbackup, string}],
result = {res, restuple}},
#ejabberd_commands{name = backup, tags = [mnesia],
- desc = "Store the database to backup file",
+ desc = "Backup the Mnesia database to a binary file",
module = ?MODULE, function = backup_mnesia,
args_desc = ["Full path for the destination backup file"],
args_example = ["/var/lib/ejabberd/database.backup"],
args = [{file, string}], result = {res, restuple}},
#ejabberd_commands{name = restore, tags = [mnesia],
- desc = "Restore the database from backup file",
+ desc = "Restore the Mnesia database from a binary backup file",
+ longdesc = "This restores immediately from a "
+ "binary backup file the internal Mnesia "
+ "database. This will consume a lot of memory if "
+ "you have a large database, you may prefer "
+ "'install_fallback'.",
module = ?MODULE, function = restore_mnesia,
args_desc = ["Full path to the backup file"],
args_example = ["/var/lib/ejabberd/database.backup"],
args = [{file, string}], result = {res, restuple}},
#ejabberd_commands{name = dump, tags = [mnesia],
- desc = "Dump the database to a text file",
+ desc = "Dump the Mnesia database to a text file",
module = ?MODULE, function = dump_mnesia,
args_desc = ["Full path for the text file"],
args_example = ["/var/lib/ejabberd/database.txt"],
args = [{file, string}], result = {res, restuple}},
#ejabberd_commands{name = dump_table, tags = [mnesia],
- desc = "Dump a table to a text file",
+ desc = "Dump a Mnesia table to a text file",
module = ?MODULE, function = dump_table,
args_desc = ["Full path for the text file", "Table name"],
args_example = ["/var/lib/ejabberd/table-muc-registered.txt", "muc_registered"],
args = [{file, string}, {table, string}], result = {res, restuple}},
#ejabberd_commands{name = load, tags = [mnesia],
- desc = "Restore the database from a text file",
+ desc = "Restore Mnesia database from a text dump file",
+ longdesc = "Restore immediately. This is not "
+ "recommended for big databases, as it will "
+ "consume much time, memory and processor. In "
+ "that case it's preferable to use 'backup' and "
+ "'install_fallback'.",
module = ?MODULE, function = load_mnesia,
args_desc = ["Full path to the text file"],
args_example = ["/var/lib/ejabberd/database.txt"],
@@ -385,7 +400,14 @@ get_commands_spec() ->
args_example = ["roster"],
args = [{table, string}], result = {res, string}},
#ejabberd_commands{name = install_fallback, tags = [mnesia],
- desc = "Install the database from a fallback file",
+ desc = "Install Mnesia database from a binary backup file",
+ longdesc = "The binary backup file is "
+ "installed as fallback: it will be used to "
+ "restore the database at the next ejabberd "
+ "start. This means that, after running this "
+ "command, you have to restart ejabberd. This "
+ "command requires less memory than
+ 'restore'.",
module = ?MODULE, function = install_fallback_mnesia,
args_desc = ["Full path to the fallback file"],
args_example = ["/var/lib/ejabberd/database.fallback"],
diff --git a/src/ejabberd_auth_sql.erl b/src/ejabberd_auth_sql.erl
index 1f7106c59..50cc1902e 100644
--- a/src/ejabberd_auth_sql.erl
+++ b/src/ejabberd_auth_sql.erl
@@ -299,6 +299,20 @@ export(_Server) ->
["username=%(LUser)s",
"server_host=%(LServer)s",
"password=%(Password)s"])];
+ (Host, {passwd, {LUser, LServer},
+ {scram, StoredKey1, ServerKey, Salt, IterationCount}})
+ when LServer == Host ->
+ Hash = sha,
+ StoredKey = scram_hash_encode(Hash, StoredKey1),
+ [?SQL("delete from users where username=%(LUser)s and %(LServer)H;"),
+ ?SQL_INSERT(
+ "users",
+ ["username=%(LUser)s",
+ "server_host=%(LServer)s",
+ "password=%(StoredKey)s",
+ "serverkey=%(ServerKey)s",
+ "salt=%(Salt)s",
+ "iterationcount=%(IterationCount)d"])];
(Host, #passwd{us = {LUser, LServer}, password = #scram{} = Scram})
when LServer == Host ->
StoredKey = scram_hash_encode(Scram#scram.hash, Scram#scram.storedkey),
diff --git a/src/ejabberd_commands.erl b/src/ejabberd_commands.erl
index c00b1469a..ddf0d3c59 100644
--- a/src/ejabberd_commands.erl
+++ b/src/ejabberd_commands.erl
@@ -94,6 +94,7 @@ get_commands_spec() ->
result_example = ok},
#ejabberd_commands{name = gen_markdown_doc_for_tags, tags = [documentation],
desc = "Generates markdown documentation for ejabberd_commands",
+ note = "added in 21.12",
module = ejabberd_commands_doc, function = generate_tags_md,
args = [{file, binary}],
result = {res, rescode},
diff --git a/src/ejabberd_ctl.erl b/src/ejabberd_ctl.erl
index 04e383d53..77595cd54 100644
--- a/src/ejabberd_ctl.erl
+++ b/src/ejabberd_ctl.erl
@@ -378,7 +378,11 @@ format_arg("", string) ->
format_arg(Arg, string) ->
NumChars = integer_to_list(length(Arg)),
Parse = "~" ++ NumChars ++ "c",
- format_arg2(Arg, Parse).
+ format_arg2(Arg, Parse);
+format_arg(Arg, Format) ->
+ S = unicode:characters_to_binary(Arg, utf8),
+ JSON = jiffy:decode(S),
+ mod_http_api:format_arg(JSON, Format).
format_arg2(Arg, Parse)->
{ok, [Arg2], _RemainingArguments} = io_lib:fread(Parse, Arg),
@@ -525,6 +529,7 @@ print_usage(Version) ->
print_usage(HelpMode, MaxC, ShCode, Version) ->
AllCommands =
[
+ {"help", ["[arguments]"], "Get help"},
{"status", [], "Get ejabberd status"},
{"stop", [], "Stop ejabberd"},
{"restart", [], "Restart ejabberd"},
diff --git a/src/ejabberd_piefxis.erl b/src/ejabberd_piefxis.erl
index 8dff06837..d62efb300 100644
--- a/src/ejabberd_piefxis.erl
+++ b/src/ejabberd_piefxis.erl
@@ -24,17 +24,15 @@
%%%----------------------------------------------------------------------
%%% Not implemented:
+%%% - PEP nodes export/import
+%%% - message archives export/import
%%% - write mod_piefxis with ejabberdctl commands
-%%% - Export from mod_offline_sql.erl
-%%% - Export from mod_private_sql.erl
-%%% - XEP-227: 6. Security Considerations
%%% - Other schemas of XInclude are not tested, and may not be imported correctly.
%%% - If a host has many users, split that host in XML files with 50 users each.
-%%%% Headers
-module(ejabberd_piefxis).
--protocol({xep, 227, '1.0'}).
+-protocol({xep, 227, '1.1'}).
-export([import_file/1, export_server/1, export_host/2]).
@@ -166,33 +164,66 @@ export_users([], _Server, _Fd) ->
export_user(User, Server, Fd) ->
Password = ejabberd_auth:get_password_s(User, Server),
LServer = jid:nameprep(Server),
- Pass = case ejabberd_auth:password_format(LServer) of
- scram -> format_scram_password(Password);
- _ -> Password
+ {PassPlain, PassScram} = case ejabberd_auth:password_format(LServer) of
+ scram -> {[], [format_scram_password(Password)]};
+ _ -> {[{<<"password">>, Password}], []}
end,
- Els = get_offline(User, Server) ++
+ Els =
+ PassScram ++
+ get_offline(User, Server) ++
get_vcard(User, Server) ++
get_privacy(User, Server) ++
get_roster(User, Server) ++
get_private(User, Server),
print(Fd, fxml:element_to_binary(
#xmlel{name = <<"user">>,
- attrs = [{<<"name">>, User},
- {<<"password">>, Pass}],
+ attrs = [{<<"name">>, User} | PassPlain],
children = Els})).
format_scram_password(#scram{hash = Hash, storedkey = StoredKey, serverkey = ServerKey,
salt = Salt, iterationcount = IterationCount}) ->
- StoredKeyB64 = base64:encode(StoredKey),
- ServerKeyB64 = base64:encode(ServerKey),
- SaltB64 = base64:encode(Salt),
- IterationCountBin = (integer_to_binary(IterationCount)),
- Hash2 = case Hash of
- sha -> <<>>;
- sha256 -> <<"sha256,">>;
- sha512 -> <<"sha512,">>
- end,
- <<"scram:", Hash2/binary, StoredKeyB64/binary, ",", ServerKeyB64/binary, ",", SaltB64/binary, ",", IterationCountBin/binary>>.
+ StoredKeyB64 = base64:encode(StoredKey),
+ ServerKeyB64 = base64:encode(ServerKey),
+ SaltB64 = base64:encode(Salt),
+ IterationCountBin = (integer_to_binary(IterationCount)),
+ MechanismB = case Hash of
+ sha -> <<"SCRAM-SHA-1">>;
+ sha256 -> <<"SCRAM-SHA-256">>;
+ sha512 -> <<"SCRAM-SHA-512">>
+ end,
+ Children =
+ [
+ #xmlel{name = <<"iter-count">>,
+ children = [{xmlcdata, IterationCountBin}]},
+ #xmlel{name = <<"salt">>,
+ children = [{xmlcdata, SaltB64}]},
+ #xmlel{name = <<"server-key">>,
+ children = [{xmlcdata, ServerKeyB64}]},
+ #xmlel{name = <<"stored-key">>,
+ children = [{xmlcdata, StoredKeyB64}]}
+ ],
+ #xmlel{name = <<"scram-credentials">>,
+ attrs = [{<<"xmlns">>, <<?NS_PIE/binary, "#scram">>},
+ {<<"mechanism">>, MechanismB}],
+ children = Children}.
+
+parse_scram_password(#xmlel{attrs = Attrs} = El) ->
+ Hash = case fxml:get_attr_s(<<"mechanism">>, Attrs) of
+ <<"SCRAM-SHA-1">> -> sha;
+ <<"SCRAM-SHA-256">> -> sha256;
+ <<"SCRAM-SHA-512">> -> sha512
+ end,
+ StoredKeyB64 = fxml:get_path_s(El, [{elem, <<"stored-key">>}, cdata]),
+ ServerKeyB64 = fxml:get_path_s(El, [{elem, <<"server-key">>}, cdata]),
+ IterationCountBin = fxml:get_path_s(El, [{elem, <<"iter-count">>}, cdata]),
+ SaltB64 = fxml:get_path_s(El, [{elem, <<"salt">>}, cdata]),
+ #scram{
+ storedkey = base64:decode(StoredKeyB64),
+ serverkey = base64:decode(ServerKeyB64),
+ salt = base64:decode(SaltB64),
+ hash = Hash,
+ iterationcount = (binary_to_integer(IterationCountBin))
+ };
parse_scram_password(PassData) ->
Split = binary:split(PassData, <<",">>, [global]),
@@ -214,26 +245,30 @@ parse_scram_password(PassData) ->
get_vcard(User, Server) ->
LUser = jid:nodeprep(User),
LServer = jid:nameprep(Server),
- case mod_vcard:get_vcard(LUser, LServer) of
+ try mod_vcard:get_vcard(LUser, LServer) of
error -> [];
Els -> Els
+ catch
+ error:{module_not_loaded, _, _} -> []
end.
-spec get_offline(binary(), binary()) -> [xmlel()].
get_offline(User, Server) ->
LUser = jid:nodeprep(User),
LServer = jid:nameprep(Server),
- case mod_offline:get_offline_els(LUser, LServer) of
+ try mod_offline:get_offline_els(LUser, LServer) of
[] ->
[];
Els ->
NewEls = lists:map(fun xmpp:encode/1, Els),
[#xmlel{name = <<"offline-messages">>, children = NewEls}]
+ catch
+ error:{module_not_loaded, _, _} -> []
end.
-spec get_privacy(binary(), binary()) -> [xmlel()].
get_privacy(User, Server) ->
- case mod_privacy:get_user_lists(User, Server) of
+ try mod_privacy:get_user_lists(User, Server) of
{ok, #privacy{default = Default,
lists = [_|_] = Lists}} ->
XLists = lists:map(
@@ -246,12 +281,14 @@ get_privacy(User, Server) ->
[xmpp:encode(#privacy_query{default = Default, lists = XLists})];
_ ->
[]
+ catch
+ error:{module_not_loaded, _, _} -> []
end.
-spec get_roster(binary(), binary()) -> [xmlel()].
get_roster(User, Server) ->
JID = jid:make(User, Server),
- case mod_roster:get_roster(User, Server) of
+ try mod_roster:get_roster(User, Server) of
[_|_] = Items ->
Subs =
lists:flatmap(
@@ -278,15 +315,19 @@ get_roster(User, Server) ->
[xmpp:encode(#roster_query{items = Rs}) | Subs];
_ ->
[]
+ catch
+ error:{module_not_loaded, _, _} -> []
end.
-spec get_private(binary(), binary()) -> [xmlel()].
get_private(User, Server) ->
- case mod_private:get_data(User, Server) of
+ try mod_private:get_data(User, Server) of
[_|_] = Els ->
[xmpp:encode(#private{sub_els = Els})];
_ ->
[]
+ catch
+ error:{module_not_loaded, _, _} -> []
end.
process(#state{xml_stream_state = XMLStreamState, fd = Fd} = State) ->
@@ -398,21 +439,10 @@ process_users([_|Els], State) ->
process_users([], State) ->
{ok, State}.
-process_user(#xmlel{name = <<"user">>, attrs = Attrs, children = Els},
+process_user(#xmlel{name = <<"user">>, attrs = Attrs, children = Els} = El,
#state{server = LServer} = State) ->
Name = fxml:get_attr_s(<<"name">>, Attrs),
- Password = fxml:get_attr_s(<<"password">>, Attrs),
- PasswordFormat = ejabberd_auth:password_format(LServer),
- Pass = case PasswordFormat of
- scram ->
- case Password of
- <<"scram:", PassData/binary>> ->
- parse_scram_password(PassData);
- P -> P
- end;
- _ -> Password
- end,
-
+ Pass = process_password(El, LServer),
case jid:nodeprep(Name) of
error ->
stop("Invalid 'user': ~ts", [Name]);
@@ -420,13 +450,29 @@ process_user(#xmlel{name = <<"user">>, attrs = Attrs, children = Els},
case ejabberd_auth:try_register(LUser, LServer, Pass) of
ok ->
process_user_els(Els, State#state{user = LUser});
- {error, invalid_password} when (Password == <<>>) ->
+ {error, invalid_password} when (Pass == <<>>) ->
process_user_els(Els, State#state{user = LUser});
{error, Err} ->
stop("Failed to create user '~ts': ~p", [Name, Err])
end
end.
+process_password(#xmlel{name = <<"user">>, attrs = Attrs} = El, LServer) ->
+ {PassPlain, PassOldScram} = case fxml:get_attr_s(<<"password">>, Attrs) of
+ <<"scram:", PassData/binary>> -> {<<"">>, PassData};
+ P -> {P, false}
+ end,
+ ScramCred = fxml:get_subtag(El, <<"scram-credentials">>),
+ PasswordFormat = ejabberd_auth:password_format(LServer),
+ case {PassPlain, PassOldScram, ScramCred, PasswordFormat} of
+ {PassPlain, false, false, plain} -> PassPlain;
+ {<<"">>, false, ScramCred, plain} -> parse_scram_password(ScramCred);
+ {<<"">>, PassOldScram, false, plain} -> parse_scram_password(PassOldScram);
+ {PassPlain, false, false, scram} -> PassPlain;
+ {<<"">>, false, ScramCred, scram} -> parse_scram_password(ScramCred);
+ {<<"">>, PassOldScram, false, scram} -> parse_scram_password(PassOldScram)
+ end.
+
process_user_els([#xmlel{} = El|Els], State) ->
case process_user_el(El, State) of
{ok, NewState} ->
diff --git a/src/ejabberd_s2s.erl b/src/ejabberd_s2s.erl
index 8057c9a35..04490071c 100644
--- a/src/ejabberd_s2s.erl
+++ b/src/ejabberd_s2s.erl
@@ -33,8 +33,8 @@
%% API
-export([start_link/0, stop/0, route/1, have_connection/1,
- get_connections_pids/1, try_register/1,
- remove_connection/2, start_connection/2, start_connection/3,
+ get_connections_pids/1,
+ start_connection/2, start_connection/3,
dirty_get_connections/0, allow_host/2,
incoming_s2s_number/0, outgoing_s2s_number/0,
stop_s2s_connections/0,
@@ -64,7 +64,7 @@
%% once a server is temporary blocked, it stay blocked for 60 seconds
--record(s2s, {fromto :: {binary(), binary()},
+-record(s2s, {fromto :: {binary(), binary()} | '_',
pid :: pid()}).
-record(state, {}).
@@ -112,24 +112,6 @@ is_temporarly_blocked(Host) ->
end
end.
--spec remove_connection({binary(), binary()}, pid()) -> ok.
-remove_connection({From, To} = FromTo, Pid) ->
- case mnesia:dirty_match_object(s2s, #s2s{fromto = FromTo, pid = Pid}) of
- [#s2s{pid = Pid}] ->
- F = fun() ->
- mnesia:delete_object(#s2s{fromto = FromTo, pid = Pid})
- end,
- case mnesia:transaction(F) of
- {atomic, _} -> ok;
- {aborted, Reason} ->
- ?ERROR_MSG("Failed to unregister s2s connection ~ts -> ~ts: "
- "Mnesia failure: ~p",
- [From, To, Reason])
- end;
- _ ->
- ok
- end.
-
-spec have_connection({binary(), binary()}) -> boolean().
have_connection(FromTo) ->
case catch mnesia:dirty_read(s2s, FromTo) of
@@ -148,31 +130,6 @@ get_connections_pids(FromTo) ->
[]
end.
--spec try_register({binary(), binary()}) -> boolean().
-try_register({From, To} = FromTo) ->
- MaxS2SConnectionsNumber = max_s2s_connections_number(FromTo),
- MaxS2SConnectionsNumberPerNode =
- max_s2s_connections_number_per_node(FromTo),
- F = fun () ->
- L = mnesia:read({s2s, FromTo}),
- NeededConnections = needed_connections_number(L,
- MaxS2SConnectionsNumber,
- MaxS2SConnectionsNumberPerNode),
- if NeededConnections > 0 ->
- mnesia:write(#s2s{fromto = FromTo, pid = self()}),
- true;
- true -> false
- end
- end,
- case mnesia:transaction(F) of
- {atomic, Res} -> Res;
- {aborted, Reason} ->
- ?ERROR_MSG("Failed to register s2s connection ~ts -> ~ts: "
- "Mnesia failure: ~p",
- [From, To, Reason]),
- false
- end.
-
-spec dirty_get_connections() -> [{binary(), binary()}].
dirty_get_connections() ->
mnesia:dirty_all_keys(s2s).
@@ -269,6 +226,8 @@ init([]) ->
{stop, Reason}
end.
+handle_call({new_connection, Args}, _From, State) ->
+ {reply, erlang:apply(fun new_connection_int/7, Args), State};
handle_call(Request, From, State) ->
?WARNING_MSG("Unexpected call from ~p: ~p", [From, Request]),
{noreply, State}.
@@ -289,6 +248,21 @@ handle_info({route, Packet}, State) ->
misc:format_exception(2, Class, Reason, StackTrace)])
end,
{noreply, State};
+handle_info({'DOWN', _Ref, process, Pid, _Reason}, State) ->
+ case mnesia:dirty_match_object(s2s, #s2s{fromto = '_', pid = Pid}) of
+ [#s2s{pid = Pid, fromto = {From, To}} = Obj] ->
+ F = fun() -> mnesia:delete_object(Obj) end,
+ case mnesia:transaction(F) of
+ {atomic, _} -> ok;
+ {aborted, Reason} ->
+ ?ERROR_MSG("Failed to unregister s2s connection for pid ~p (~ts -> ~ts):"
+ "Mnesia failure: ~p",
+ [Pid, From, To, Reason])
+ end,
+ {noreply, State};
+ _ ->
+ {noreply, State}
+ end;
handle_info(Info, State) ->
?WARNING_MSG("Unexpected info: ~p", [Info]),
{noreply, State}.
@@ -458,6 +432,18 @@ open_several_connections(N, MyServer, Server, From,
integer(), integer(), [proplists:property()]) -> [pid()].
new_connection(MyServer, Server, From, FromTo,
MaxS2SConnectionsNumber, MaxS2SConnectionsNumberPerNode, Opts) ->
+ case whereis(ejabberd_s2s) == self() of
+ true ->
+ new_connection_int(MyServer, Server, From, FromTo,
+ MaxS2SConnectionsNumber, MaxS2SConnectionsNumberPerNode, Opts);
+ false ->
+ gen_server:call(ejabberd_s2s, {new_connection, [MyServer, Server, From, FromTo,
+ MaxS2SConnectionsNumber,
+ MaxS2SConnectionsNumberPerNode, Opts]})
+ end.
+
+new_connection_int(MyServer, Server, From, FromTo,
+ MaxS2SConnectionsNumber, MaxS2SConnectionsNumberPerNode, Opts) ->
{ok, Pid} = ejabberd_s2s_out:start(MyServer, Server, Opts),
F = fun() ->
L = mnesia:read({s2s, FromTo}),
@@ -474,6 +460,7 @@ new_connection(MyServer, Server, From, FromTo,
case TRes of
{atomic, Pid1} ->
if Pid1 == Pid ->
+ erlang:monitor(process, Pid),
ejabberd_s2s_out:connect(Pid);
true ->
ejabberd_s2s_out:stop_async(Pid)
diff --git a/src/ejabberd_s2s_out.erl b/src/ejabberd_s2s_out.erl
index d58396533..f057705ed 100644
--- a/src/ejabberd_s2s_out.erl
+++ b/src/ejabberd_s2s_out.erl
@@ -318,7 +318,6 @@ handle_info(Info, #{server_host := ServerHost} = State) ->
terminate(Reason, #{server := LServer,
remote_server := RServer} = State) ->
- ejabberd_s2s:remove_connection({LServer, RServer}, self()),
State1 = case Reason of
normal -> State;
_ -> State#{stop_reason => internal_failure}
@@ -351,21 +350,12 @@ bounce_queue(State) ->
end, State).
-spec bounce_message_queue({binary(), binary()}, state()) -> state().
-bounce_message_queue({LServer, RServer} = FromTo, State) ->
- Pids = ejabberd_s2s:get_connections_pids(FromTo),
- case lists:member(self(), Pids) of
- true ->
- ?WARNING_MSG("Outgoing s2s connection ~ts -> ~ts is supposed "
- "to be unregistered, but pid ~p still presents "
- "in 's2s' table", [LServer, RServer, self()]),
- State;
- false ->
- receive {route, Pkt} ->
- State1 = bounce_packet(Pkt, State),
- bounce_message_queue(FromTo, State1)
- after 0 ->
- State
- end
+bounce_message_queue(FromTo, State) ->
+ receive {route, Pkt} ->
+ State1 = bounce_packet(Pkt, State),
+ bounce_message_queue(FromTo, State1)
+ after 0 ->
+ State
end.
-spec bounce_packet(xmpp_element(), state()) -> state().
diff --git a/src/ejabberd_sql_pt.erl b/src/ejabberd_sql_pt.erl
index 130228fe1..d131570f7 100644
--- a/src/ejabberd_sql_pt.erl
+++ b/src/ejabberd_sql_pt.erl
@@ -564,15 +564,23 @@ make_sql_upsert(Table, ParseRes, Pos) ->
[]
end,
erl_syntax:fun_expr(
- [erl_syntax:clause(
- [erl_syntax:atom(pgsql), erl_syntax:variable("__Version")],
- [erl_syntax:infix_expr(
- erl_syntax:variable("__Version"),
- erl_syntax:operator('>='),
- erl_syntax:integer(90100))],
- [make_sql_upsert_pgsql901(Table, ParseRes),
- erl_syntax:atom(ok)])] ++
- MySqlReplace ++
+ [erl_syntax:clause(
+ [erl_syntax:atom(pgsql), erl_syntax:variable("__Version")],
+ [erl_syntax:infix_expr(
+ erl_syntax:variable("__Version"),
+ erl_syntax:operator('>='),
+ erl_syntax:integer(90500))],
+ [make_sql_upsert_pgsql905(Table, ParseRes),
+ erl_syntax:atom(ok)]),
+ erl_syntax:clause(
+ [erl_syntax:atom(pgsql), erl_syntax:variable("__Version")],
+ [erl_syntax:infix_expr(
+ erl_syntax:variable("__Version"),
+ erl_syntax:operator('>='),
+ erl_syntax:integer(90100))],
+ [make_sql_upsert_pgsql901(Table, ParseRes),
+ erl_syntax:atom(ok)])] ++
+ MySqlReplace ++
[erl_syntax:clause(
[erl_syntax:underscore(), erl_syntax:underscore()],
none,
@@ -713,6 +721,57 @@ make_sql_upsert_pgsql901(Table, ParseRes0) ->
erl_syntax:atom(sql_query_t),
[Upsert]).
+make_sql_upsert_pgsql905(Table, ParseRes0) ->
+ ParseRes = lists:map(
+ fun({"family", A2, A3}) -> {"\"family\"", A2, A3};
+ (Other) -> Other
+ end, ParseRes0),
+ Vals =
+ lists:map(
+ fun({_Field, _, ST}) ->
+ ST
+ end, ParseRes),
+ Fields =
+ lists:map(
+ fun({Field, _, _ST}) ->
+ #state{'query' = [{str, Field}]}
+ end, ParseRes),
+ SPairs =
+ lists:flatmap(
+ fun({_Field, key, _ST}) ->
+ [];
+ ({_Field, {false}, _ST}) ->
+ [];
+ ({Field, {true}, ST}) ->
+ [ST#state{
+ 'query' = [{str, Field}, {str, "="}] ++ ST#state.'query'
+ }]
+ end, ParseRes),
+ Set = join_states(SPairs, ", "),
+ KeyFields =
+ lists:flatmap(
+ fun({Field, key, _ST}) ->
+ [#state{'query' = [{str, Field}]}];
+ ({_Field, _, _ST}) ->
+ []
+ end, ParseRes),
+ State =
+ concat_states(
+ [#state{'query' = [{str, "INSERT INTO "}, {str, Table}, {str, "("}]},
+ join_states(Fields, ", "),
+ #state{'query' = [{str, ") VALUES ("}]},
+ join_states(Vals, ", "),
+ #state{'query' = [{str, ") ON CONFLICT ("}]},
+ join_states(KeyFields, ", "),
+ #state{'query' = [{str, ") DO UPDATE SET "}]},
+ Set
+ ]),
+ Upsert = make_sql_query(State),
+ erl_syntax:application(
+ erl_syntax:atom(ejabberd_sql),
+ erl_syntax:atom(sql_query_t),
+ [Upsert]).
+
check_upsert(ParseRes, Pos) ->
Set =
diff --git a/src/ejd2sql.erl b/src/ejd2sql.erl
index ad0cc5e88..427e13087 100644
--- a/src/ejd2sql.erl
+++ b/src/ejd2sql.erl
@@ -73,11 +73,16 @@ export(Server, Output) ->
end, Modules),
close_output(Output, IO).
-export(Server, Output, Module1) ->
- Module = case Module1 of
- mod_pubsub -> pubsub_db;
- _ -> Module1
- end,
+export(Server, Output, mod_mam = M1) ->
+ MucServices = gen_mod:get_module_opt_hosts(Server, mod_muc),
+ [export2(MucService, Output, M1, M1) || MucService <- MucServices],
+ export2(Server, Output, M1, M1);
+export(Server, Output, mod_pubsub = M1) ->
+ export2(Server, Output, M1, pubsub_db);
+export(Server, Output, M1) ->
+ export2(Server, Output, M1, M1).
+
+export2(Server, Output, Module1, Module) ->
SQLMod = gen_mod:db_mod(sql, Module),
LServer = jid:nameprep(iolist_to_binary(Server)),
IO = prepare_output(Output),
diff --git a/src/gen_pubsub_node.erl b/src/gen_pubsub_node.erl
index 625e490fc..3f83fe48f 100644
--- a/src/gen_pubsub_node.erl
+++ b/src/gen_pubsub_node.erl
@@ -133,6 +133,10 @@
{result, {[itemId()], [itemId()]}
}.
+-callback remove_expired_items(NodeIdx :: nodeIdx(),
+ Seconds :: infinity | non_neg_integer()) ->
+ {result, [itemId()]}.
+
-callback get_node_affiliations(NodeIdx :: nodeIdx()) ->
{result, [{ljid(), affiliation()}]}.
diff --git a/src/mod_admin_update_sql.erl b/src/mod_admin_update_sql.erl
index 4e932fe83..02beb4bf8 100644
--- a/src/mod_admin_update_sql.erl
+++ b/src/mod_admin_update_sql.erl
@@ -140,6 +140,7 @@ update_tables(State) ->
add_sh_column(State, "sr_group"),
add_pkey(State, "sr_group", ["server_host", "name"]),
+ create_unique_index(State, "sr_group", "i_sr_group_sh_name", ["server_host", "name"]),
drop_sh_default(State, "sr_group"),
add_sh_column(State, "sr_user"),
@@ -147,6 +148,7 @@ update_tables(State) ->
drop_index(State, "i_sr_user_jid"),
drop_index(State, "i_sr_user_grp"),
add_pkey(State, "sr_user", ["server_host", "jid", "grp"]),
+ create_unique_index(State, "sr_user", "i_sr_user_sh_jid_grp", ["server_host", "jid", "grp"]),
create_index(State, "sr_user", "i_sr_user_sh_jid", ["server_host", "jid"]),
create_index(State, "sr_user", "i_sr_user_sh_grp", ["server_host", "grp"]),
drop_sh_default(State, "sr_user"),
diff --git a/src/mod_caps.erl b/src/mod_caps.erl
index c8f548169..bc48dac6f 100644
--- a/src/mod_caps.erl
+++ b/src/mod_caps.erl
@@ -49,7 +49,8 @@
handle_cast/2, terminate/2, code_change/3]).
-export([user_send_packet/1, user_receive_packet/1,
- c2s_presence_in/2, mod_opt_type/1, mod_options/1, mod_doc/0]).
+ c2s_presence_in/2, c2s_copy_session/2,
+ mod_opt_type/1, mod_options/1, mod_doc/0]).
-include("logger.hrl").
@@ -274,6 +275,13 @@ c2s_presence_in(C2SState,
C2SState#{caps_resources => NewRs}
end.
+-spec c2s_copy_session(ejabberd_c2s:state(), ejabberd_c2s:state())
+ -> ejabberd_c2s:state().
+c2s_copy_session(C2SState, #{caps_resources := Rs}) ->
+ C2SState#{caps_resources => Rs};
+c2s_copy_session(C2SState, _) ->
+ C2SState.
+
-spec depends(binary(), gen_mod:opts()) -> [{module(), hard | soft}].
depends(_Host, _Opts) ->
[].
@@ -304,6 +312,8 @@ init([Host|_]) ->
caps_stream_features, 75),
ejabberd_hooks:add(s2s_in_post_auth_features, Host, ?MODULE,
caps_stream_features, 75),
+ ejabberd_hooks:add(c2s_copy_session, Host, ?MODULE,
+ c2s_copy_session, 75),
ejabberd_hooks:add(disco_local_features, Host, ?MODULE,
disco_features, 75),
ejabberd_hooks:add(disco_local_identity, Host, ?MODULE,
@@ -341,6 +351,8 @@ terminate(_Reason, State) ->
?MODULE, caps_stream_features, 75),
ejabberd_hooks:delete(s2s_in_post_auth_features, Host,
?MODULE, caps_stream_features, 75),
+ ejabberd_hooks:delete(c2s_copy_session, Host, ?MODULE,
+ c2s_copy_session, 75),
ejabberd_hooks:delete(disco_local_features, Host,
?MODULE, disco_features, 75),
ejabberd_hooks:delete(disco_local_identity, Host,
diff --git a/src/mod_conversejs.erl b/src/mod_conversejs.erl
new file mode 100644
index 000000000..8683d60ab
--- /dev/null
+++ b/src/mod_conversejs.erl
@@ -0,0 +1,157 @@
+%%%----------------------------------------------------------------------
+%%% File : mod_conversejs.erl
+%%% Author : Alexey Shchepin <alexey@process-one.net>
+%%% Purpose : Serve simple page for Converse.js XMPP web browser client
+%%% Created : 8 Nov 2021 by Alexey Shchepin <alexey@process-one.net>
+%%%
+%%%
+%%% ejabberd, Copyright (C) 2002-2021 ProcessOne
+%%%
+%%% This program is free software; you can redistribute it and/or
+%%% modify it under the terms of the GNU General Public License as
+%%% published by the Free Software Foundation; either version 2 of the
+%%% License, or (at your option) any later version.
+%%%
+%%% This program is distributed in the hope that it will be useful,
+%%% but WITHOUT ANY WARRANTY; without even the implied warranty of
+%%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+%%% General Public License for more details.
+%%%
+%%% You should have received a copy of the GNU General Public License along
+%%% with this program; if not, write to the Free Software Foundation, Inc.,
+%%% 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+%%%
+%%%----------------------------------------------------------------------
+
+-module(mod_conversejs).
+
+-author('alexey@process-one.net').
+
+-behaviour(gen_mod).
+
+-export([start/2, stop/1, reload/3, process/2, depends/2,
+ mod_opt_type/1, mod_options/1, mod_doc/0]).
+
+-include_lib("xmpp/include/xmpp.hrl").
+-include("logger.hrl").
+-include("ejabberd_http.hrl").
+-include("translate.hrl").
+-include("ejabberd_web_admin.hrl").
+
+start(_Host, _Opts) ->
+ ok.
+
+stop(_Host) ->
+ ok.
+
+reload(_Host, _NewOpts, _OldOpts) ->
+ ok.
+
+depends(_Host, _Opts) ->
+ [].
+
+process([], #request{method = 'GET'}) ->
+ Host = ejabberd_config:get_myname(),
+ Domain = gen_mod:get_module_opt(Host, ?MODULE, default_domain),
+ Script = gen_mod:get_module_opt(Host, ?MODULE, conversejs_script),
+ CSS = gen_mod:get_module_opt(Host, ?MODULE, conversejs_css),
+ Init = [{<<"discover_connection_methods">>, false},
+ {<<"jid">>, Domain},
+ {<<"default_domain">>, Domain},
+ {<<"domain_placeholder">>, Domain},
+ {<<"view_mode">>, <<"fullscreen">>}],
+ Init2 =
+ case gen_mod:get_module_opt(Host, ?MODULE, websocket_url) of
+ undefined -> Init;
+ WSURL -> [{<<"websocket_url">>, WSURL} | Init]
+ end,
+ Init3 =
+ case gen_mod:get_module_opt(Host, ?MODULE, bosh_service_url) of
+ undefined -> Init2;
+ BoshURL -> [{<<"bosh_service_url">>, BoshURL} | Init2]
+ end,
+ {200, [html],
+ [<<"<!DOCTYPE html>">>,
+ <<"<html>">>,
+ <<"<head>">>,
+ <<"<meta charset='utf-8'>">>,
+ <<"<link rel='stylesheet' type='text/css' media='screen' href='">>,
+ fxml:crypt(CSS), <<"'>">>,
+ <<"<script src='">>, fxml:crypt(Script), <<"' charset='utf-8'></script>">>,
+ <<"</head>">>,
+ <<"<body>">>,
+ <<"<script>">>,
+ <<"converse.initialize(">>, jiffy:encode({Init3}), <<");">>,
+ <<"</script>">>,
+ <<"</body>">>,
+ <<"</html>">>]};
+process(_, _) ->
+ ejabberd_web:error(not_found).
+
+mod_opt_type(bosh_service_url) ->
+ econf:either(undefined, econf:binary());
+mod_opt_type(websocket_url) ->
+ econf:either(undefined, econf:binary());
+mod_opt_type(conversejs_script) ->
+ econf:binary();
+mod_opt_type(conversejs_css) ->
+ econf:binary();
+mod_opt_type(default_domain) ->
+ econf:binary().
+
+mod_options(_) ->
+ [{bosh_service_url, undefined},
+ {websocket_url, undefined},
+ {default_domain, ejabberd_config:get_myname()},
+ {conversejs_script, <<"https://cdn.conversejs.org/dist/converse.min.js">>},
+ {conversejs_css, <<"https://cdn.conversejs.org/dist/converse.min.css">>}].
+
+mod_doc() ->
+ #{desc =>
+ [?T("This module serves a simple page for the "
+ "https://conversejs.org/[Converse] XMPP web browser client."), "",
+ ?T("This module is available since ejabberd 21.12."), "",
+ ?T("To use this module, in addition to adding it to the 'modules' "
+ "section, you must also enable it in 'listen' -> 'ejabberd_http' -> "
+ "http://../listen-options/#request-handlers[request_handlers]."), "",
+ ?T("You must also setup either the option 'websocket_url' or 'bosh_service_url'."), "",
+ ?T("By default, the options 'conversejs_css' and 'conversejs_script'"
+ " point to the public Converse.js client. Alternatively, you can"
+ " host the client locally using _`mod_http_fileserver`_.")
+ ],
+ example =>
+ ["listen:",
+ " -",
+ " port: 5280",
+ " module: ejabberd_http",
+ " request_handlers:",
+ " /websocket: ejabberd_http_ws",
+ " /conversejs: mod_conversejs",
+ "",
+ "modules:",
+ " mod_conversejs:",
+ " websocket_url: \"ws://example.org:5280/websocket\""],
+ opts =>
+ [{websocket_url,
+ #{value => ?T("WebsocketURL"),
+ desc =>
+ ?T("A websocket URL to which Converse.js can connect to.")}},
+ {bosh_service_url,
+ #{value => ?T("BoshURL"),
+ desc =>
+ ?T("BOSH service URL to which Converse.js can connect to.")}},
+ {default_domain,
+ #{value => ?T("Domain"),
+ desc =>
+ ?T("Specify a domain to act as the default for user JIDs. "
+ "The default value is the first domain defined in the "
+ "ejabberd configuration file.")}},
+ {conversejs_script,
+ #{value => ?T("URL"),
+ desc =>
+ ?T("Converse.js main script URL.")}},
+ {conversejs_css,
+ #{value => ?T("URL"),
+ desc =>
+ ?T("Converse.js CSS URL.")}}]
+ }.
diff --git a/src/mod_conversejs_opt.erl b/src/mod_conversejs_opt.erl
new file mode 100644
index 000000000..9e53978ea
--- /dev/null
+++ b/src/mod_conversejs_opt.erl
@@ -0,0 +1,41 @@
+%% Generated automatically
+%% DO NOT EDIT: run `make options` instead
+
+-module(mod_conversejs_opt).
+
+-export([bosh_service_url/1]).
+-export([conversejs_css/1]).
+-export([conversejs_script/1]).
+-export([default_domain/1]).
+-export([websocket_url/1]).
+
+-spec bosh_service_url(gen_mod:opts() | global | binary()) -> 'undefined' | binary().
+bosh_service_url(Opts) when is_map(Opts) ->
+ gen_mod:get_opt(bosh_service_url, Opts);
+bosh_service_url(Host) ->
+ gen_mod:get_module_opt(Host, mod_conversejs, bosh_service_url).
+
+-spec conversejs_css(gen_mod:opts() | global | binary()) -> binary().
+conversejs_css(Opts) when is_map(Opts) ->
+ gen_mod:get_opt(conversejs_css, Opts);
+conversejs_css(Host) ->
+ gen_mod:get_module_opt(Host, mod_conversejs, conversejs_css).
+
+-spec conversejs_script(gen_mod:opts() | global | binary()) -> binary().
+conversejs_script(Opts) when is_map(Opts) ->
+ gen_mod:get_opt(conversejs_script, Opts);
+conversejs_script(Host) ->
+ gen_mod:get_module_opt(Host, mod_conversejs, conversejs_script).
+
+-spec default_domain(gen_mod:opts() | global | binary()) -> binary().
+default_domain(Opts) when is_map(Opts) ->
+ gen_mod:get_opt(default_domain, Opts);
+default_domain(Host) ->
+ gen_mod:get_module_opt(Host, mod_conversejs, default_domain).
+
+-spec websocket_url(gen_mod:opts() | global | binary()) -> 'undefined' | binary().
+websocket_url(Opts) when is_map(Opts) ->
+ gen_mod:get_opt(websocket_url, Opts);
+websocket_url(Host) ->
+ gen_mod:get_module_opt(Host, mod_conversejs, websocket_url).
+
diff --git a/src/mod_http_api.erl b/src/mod_http_api.erl
index 427833584..023df39ca 100644
--- a/src/mod_http_api.erl
+++ b/src/mod_http_api.erl
@@ -30,6 +30,7 @@
-behaviour(gen_mod).
-export([start/2, stop/1, reload/3, process/2, depends/2,
+ format_arg/2,
mod_options/1, mod_doc/0]).
-include_lib("xmpp/include/xmpp.hrl").
diff --git a/src/mod_mam.erl b/src/mod_mam.erl
index abb2333cc..9bf154f58 100644
--- a/src/mod_mam.erl
+++ b/src/mod_mam.erl
@@ -28,6 +28,7 @@
-protocol({xep, 313, '0.6.1'}).
-protocol({xep, 334, '0.2'}).
-protocol({xep, 359, '0.5.0'}).
+-protocol({xep, 441, '0.2.0'}).
-behaviour(gen_mod).
diff --git a/src/mod_mqtt_session.erl b/src/mod_mqtt_session.erl
index ca025e3d2..e7737804e 100644
--- a/src/mod_mqtt_session.erl
+++ b/src/mod_mqtt_session.erl
@@ -1134,8 +1134,8 @@ is_expired(#publish{meta = Meta, properties = Props} = Pkt) ->
%%% Authentication
%%%===================================================================
-spec parse_credentials(connect()) -> {ok, jid:jid()} | {error, reason_code()}.
-parse_credentials(#connect{client_id = <<>>}) ->
- parse_credentials(#connect{client_id = p1_rand:get_string()});
+parse_credentials(#connect{client_id = <<>>} = C) ->
+ parse_credentials(C#connect{client_id = p1_rand:get_string()});
parse_credentials(#connect{username = <<>>, client_id = ClientID}) ->
Host = ejabberd_config:get_myname(),
JID = case jid:make(ClientID, Host) of
diff --git a/src/mod_muc.erl b/src/mod_muc.erl
index b2ebc5c61..72f386b00 100644
--- a/src/mod_muc.erl
+++ b/src/mod_muc.erl
@@ -69,6 +69,7 @@
get_online_rooms_by_user/3,
can_use_nick/4,
get_subscribed_rooms/2,
+ remove_user/2,
procname/2,
route/1, unhibernate_room/3]).
@@ -122,6 +123,8 @@
start(Host, Opts) ->
case mod_muc_sup:start(Host) of
{ok, _} ->
+ ejabberd_hooks:add(remove_user, Host, ?MODULE,
+ remove_user, 50),
MyHosts = gen_mod:get_opt_hosts(Opts),
Mod = gen_mod:db_mod(Opts, ?MODULE),
RMod = gen_mod:ram_db_mod(Opts, ?MODULE),
@@ -133,6 +136,8 @@ start(Host, Opts) ->
end.
stop(Host) ->
+ ejabberd_hooks:delete(remove_user, Host, ?MODULE,
+ remove_user, 50),
Proc = mod_muc_sup:procname(Host),
supervisor:terminate_child(ejabberd_gen_mod_sup, Proc),
supervisor:delete_child(ejabberd_gen_mod_sup, Proc).
@@ -1122,6 +1127,32 @@ count_online_rooms(ServerHost, Host) ->
RMod = gen_mod:ram_db_mod(ServerHost, ?MODULE),
RMod:count_online_rooms(ServerHost, Host).
+-spec remove_user(binary(), binary()) -> ok.
+remove_user(User, Server) ->
+ LUser = jid:nodeprep(User),
+ LServer = jid:nameprep(Server),
+ Mod = gen_mod:db_mod(LServer, ?MODULE),
+ case erlang:function_exported(Mod, remove_user, 2) of
+ true ->
+ Mod:remove_user(LUser, LServer);
+ false ->
+ ok
+ end,
+ JID = jid:make(User, Server),
+ lists:foreach(
+ fun(Host) ->
+ lists:foreach(
+ fun({_, _, Pid}) ->
+ mod_muc_room:change_item_async(
+ Pid, JID, affiliation, none, <<"User removed">>),
+ mod_muc_room:change_item_async(
+ Pid, JID, role, none, <<"User removed">>)
+ end,
+ get_online_rooms(LServer, Host))
+ end,
+ gen_mod:get_module_opt_hosts(LServer, mod_muc)),
+ ok.
+
opts_to_binary(Opts) ->
lists:map(
fun({title, Title}) ->
@@ -1225,6 +1256,8 @@ mod_opt_type(user_message_shaper) ->
econf:atom();
mod_opt_type(user_presence_shaper) ->
econf:atom();
+mod_opt_type(cleanup_affiliations_on_start) ->
+ econf:bool();
mod_opt_type(default_room_options) ->
econf:options(
#{allow_change_subj => econf:bool(),
@@ -1302,6 +1335,7 @@ mod_options(Host) ->
{preload_rooms, true},
{hibernation_timeout, infinity},
{vcard, undefined},
+ {cleanup_affiliations_on_start, false},
{default_room_options,
[{allow_change_subj,true},
{allow_private_messages,true},
@@ -1580,6 +1614,11 @@ mod_doc() ->
" -",
" work: true",
" street: Elm Street"]}]}},
+ {cleanup_affiliations_on_start,
+ #{value => "true | false",
+ desc =>
+ ?T("Remove affiliations for non-existing local users on startup. "
+ "The default value is 'false'.")}},
{default_room_options,
#{value => ?T("Options"),
desc =>
diff --git a/src/mod_muc_admin.erl b/src/mod_muc_admin.erl
index 2abeee45c..ac2d887fe 100644
--- a/src/mod_muc_admin.erl
+++ b/src/mod_muc_admin.erl
@@ -40,8 +40,11 @@
change_room_option/4, get_room_options/2,
set_room_affiliation/4, get_room_affiliations/2, get_room_affiliation/3,
web_menu_main/2, web_page_main/2, web_menu_host/3,
- subscribe_room/4, unsubscribe_room/2, get_subscribers/2,
- web_page_host/3, mod_options/1, get_commands_spec/0, find_hosts/1]).
+ subscribe_room/4, subscribe_room_many/3,
+ unsubscribe_room/2, get_subscribers/2,
+ web_page_host/3,
+ mod_opt_type/1, mod_options/1,
+ get_commands_spec/0, find_hosts/1]).
-include("logger.hrl").
-include_lib("xmpp/include/xmpp.hrl").
@@ -281,7 +284,7 @@ get_commands_spec() ->
#ejabberd_commands{name = send_direct_invitation, tags = [muc_room],
desc = "Send a direct invitation to several destinations",
- longdesc = "Since ejabberd 20.10, this command is "
+ longdesc = "Since ejabberd 20.12, this command is "
"asynchronous: the API call may return before the "
"server has send all the invitations.\n\n"
"Password and Message can also be: none. "
@@ -331,6 +334,26 @@ get_commands_spec() ->
args = [{user, binary}, {nick, binary}, {room, binary},
{nodes, binary}],
result = {nodes, {list, {node, string}}}},
+ #ejabberd_commands{name = subscribe_room_many, tags = [muc_room],
+ desc = "Subscribe several users to a MUC conference",
+ longdesc = "This command accept up to 50 users at once (this is configurable with `subscribe_room_many_max_users` option)",
+ module = ?MODULE, function = subscribe_room_many,
+ args_desc = ["Users JIDs and nicks",
+ "the room to subscribe",
+ "nodes separated by commas: ,"],
+ args_example = [[{"tom@localhost", "Tom"},
+ {"jerry@localhost", "Jerry"}],
+ "room1@conference.localhost",
+ "urn:xmpp:mucsub:nodes:messages,urn:xmpp:mucsub:nodes:affiliations"],
+ args = [{users, {list,
+ {user, {tuple,
+ [{jid, binary},
+ {nick, binary}
+ ]}}
+ }},
+ {room, binary},
+ {nodes, binary}],
+ result = {res, rescode}},
#ejabberd_commands{name = unsubscribe_room, tags = [muc_room],
desc = "Unsubscribe from a MUC conference",
module = ?MODULE, function = unsubscribe_room,
@@ -710,7 +733,7 @@ create_room_with_opts(Name1, Host1, ServerHost1, CustomRoomOpts) ->
maybe_store_room(ServerHost, Host, Name, RoomOpts) ->
case proplists:get_bool(persistent, RoomOpts) of
true ->
- {atomic, ok} = mod_muc:store_room(ServerHost, Host, Name, RoomOpts),
+ {atomic, _} = mod_muc:store_room(ServerHost, Host, Name, RoomOpts),
ok;
false ->
ok
@@ -860,7 +883,14 @@ get_online_rooms(ServiceArg) ->
|| {RoomName, RoomHost, Pid} <- mod_muc:get_online_rooms(Host)]
end, Hosts).
-get_all_rooms(Host) ->
+get_all_rooms(ServiceArg) ->
+ Hosts = find_services(ServiceArg),
+ lists:flatmap(
+ fun(Host) ->
+ get_all_rooms2(Host)
+ end, Hosts).
+
+get_all_rooms2(Host) ->
ServerHost = ejabberd_router:host_of_route(Host),
OnlineRooms = get_online_rooms(Host),
OnlineMap = lists:foldl(
@@ -1324,6 +1354,18 @@ subscribe_room(User, Nick, Room, Nodes) ->
throw({error, "Malformed room JID"})
end.
+subscribe_room_many(Users, Room, Nodes) ->
+ MaxUsers = mod_muc_admin_opt:subscribe_room_many_max_users(global),
+ if
+ length(Users) > MaxUsers ->
+ throw({error, "Too many users in subscribe_room_many command"});
+ true ->
+ lists:foreach(
+ fun({User, Nick}) ->
+ subscribe_room(User, Nick, Room, Nodes)
+ end, Users)
+ end.
+
unsubscribe_room(User, Room) ->
try jid:decode(Room) of
#jid{luser = Name, lserver = Host} when Name /= <<"">> ->
@@ -1406,11 +1448,22 @@ find_hosts(ServerHost) ->
[]
end.
-mod_options(_) -> [].
+mod_opt_type(subscribe_room_many_max_users) ->
+ econf:int().
+
+mod_options(_) ->
+ [{subscribe_room_many_max_users, 50}].
mod_doc() ->
#{desc =>
[?T("This module provides commands to administer local MUC "
"services and their MUC rooms. It also provides simple "
"WebAdmin pages to view the existing rooms."), "",
- ?T("This module depends on _`mod_muc`_.")]}.
+ ?T("This module depends on _`mod_muc`_.")],
+ opts =>
+ [{subscribe_room_many_max_users,
+ #{value => ?T("Number"),
+ desc =>
+ ?T("How many users can be subscribed to a room at once using "
+ "the 'subscribe_room_many' command. "
+ "The default value is '50'.")}}]}.
diff --git a/src/mod_muc_admin_opt.erl b/src/mod_muc_admin_opt.erl
new file mode 100644
index 000000000..18ca64af7
--- /dev/null
+++ b/src/mod_muc_admin_opt.erl
@@ -0,0 +1,13 @@
+%% Generated automatically
+%% DO NOT EDIT: run `make options` instead
+
+-module(mod_muc_admin_opt).
+
+-export([subscribe_room_many_max_users/1]).
+
+-spec subscribe_room_many_max_users(gen_mod:opts() | global | binary()) -> integer().
+subscribe_room_many_max_users(Opts) when is_map(Opts) ->
+ gen_mod:get_opt(subscribe_room_many_max_users, Opts);
+subscribe_room_many_max_users(Host) ->
+ gen_mod:get_module_opt(Host, mod_muc_admin, subscribe_room_many_max_users).
+
diff --git a/src/mod_muc_opt.erl b/src/mod_muc_opt.erl
index 760a5d7c8..4b9e8b806 100644
--- a/src/mod_muc_opt.erl
+++ b/src/mod_muc_opt.erl
@@ -9,6 +9,7 @@
-export([access_mam/1]).
-export([access_persistent/1]).
-export([access_register/1]).
+-export([cleanup_affiliations_on_start/1]).
-export([db_type/1]).
-export([default_room_options/1]).
-export([hibernation_timeout/1]).
@@ -73,6 +74,12 @@ access_register(Opts) when is_map(Opts) ->
access_register(Host) ->
gen_mod:get_module_opt(Host, mod_muc, access_register).
+-spec cleanup_affiliations_on_start(gen_mod:opts() | global | binary()) -> boolean().
+cleanup_affiliations_on_start(Opts) when is_map(Opts) ->
+ gen_mod:get_opt(cleanup_affiliations_on_start, Opts);
+cleanup_affiliations_on_start(Host) ->
+ gen_mod:get_module_opt(Host, mod_muc, cleanup_affiliations_on_start).
+
-spec db_type(gen_mod:opts() | global | binary()) -> atom().
db_type(Opts) when is_map(Opts) ->
gen_mod:get_opt(db_type, Opts);
diff --git a/src/mod_muc_room.erl b/src/mod_muc_room.erl
index 035e851fd..aaf3e8895 100644
--- a/src/mod_muc_room.erl
+++ b/src/mod_muc_room.erl
@@ -27,6 +27,8 @@
-author('alexey@process-one.net').
+-protocol({xep, 317, '0.1'}).
+
-behaviour(p1_fsm).
%% External exports
@@ -48,6 +50,7 @@
set_config/2,
get_state/1,
change_item/5,
+ change_item_async/5,
config_reloaded/1,
subscribe/4,
unsubscribe/2,
@@ -76,6 +79,12 @@
-define(DEFAULT_MAX_USERS_PRESENCE,1000).
+-define(MUC_HAT_ADD_CMD, <<"http://prosody.im/protocol/hats#add">>).
+-define(MUC_HAT_REMOVE_CMD, <<"http://prosody.im/protocol/hats#remove">>).
+-define(MUC_HAT_LIST_CMD, <<"p1:hats#list">>).
+-define(MAX_HATS_USERS, 100).
+-define(MAX_HATS_PER_USER, 10).
+
%-define(DBGFSM, true).
-ifdef(DBGFSM).
@@ -194,6 +203,11 @@ change_item(Pid, JID, Type, AffiliationOrRole, Reason) ->
{error, notfound}
end.
+-spec change_item_async(pid(), jid(), affiliation | role, affiliation() | role(), binary()) -> ok.
+change_item_async(Pid, JID, Type, AffiliationOrRole, Reason) ->
+ p1_fsm:send_all_state_event(
+ Pid, {process_item_change, {JID, Type, AffiliationOrRole, Reason}, undefined}).
+
-spec get_state(pid()) -> {ok, state()} | {error, notfound | timeout}.
get_state(Pid) ->
try p1_fsm:sync_send_all_state_event(Pid, get_state)
@@ -298,7 +312,8 @@ init([Host, ServerHost, Access, Room, HistorySize, RoomShaper, Opts, QueueType])
room_shaper = Shaper}),
add_to_log(room_existence, started, State),
ejabberd_hooks:run(start_room, ServerHost, [ServerHost, Room, Host]),
- {ok, normal_state, reset_hibernate_timer(State)}.
+ State1 = cleanup_affiliations(State),
+ {ok, normal_state, reset_hibernate_timer(State1)}.
normal_state({route, <<"">>,
#message{from = From, type = Type, lang = Lang} = Packet},
@@ -446,6 +461,8 @@ normal_state({route, <<"">>,
process_iq_mucsub(From, IQ, StateData);
#xcaptcha{} ->
process_iq_captcha(From, IQ, StateData);
+ #adhoc_command{} ->
+ process_iq_adhoc(From, IQ, StateData);
_ ->
Txt = ?T("The feature requested is not "
"supported by the conference"),
@@ -664,6 +681,16 @@ handle_event({set_affiliations, Affiliations},
StateName, StateData) ->
NewStateData = set_affiliations(Affiliations, StateData),
{next_state, StateName, NewStateData};
+handle_event({process_item_change, Item, UJID}, StateName, StateData) ->
+ case process_item_change(Item, StateData, UJID) of
+ {error, _} ->
+ {next_state, StateName, StateData};
+ StateData ->
+ {next_state, StateName, StateData};
+ NSD ->
+ store_room(NSD),
+ {next_state, StateName, NSD}
+ end;
handle_event(_Event, StateName, StateData) ->
{next_state, StateName, StateData}.
@@ -712,6 +739,8 @@ handle_sync_event({process_item_change, Item, UJID}, _From, StateName, StateData
case process_item_change(Item, StateData, UJID) of
{error, _} = Err ->
{reply, Err, StateName, StateData};
+ StateData ->
+ {reply, {ok, StateData}, StateName, StateData};
NSD ->
store_room(NSD),
{reply, {ok, NSD}, StateName, NSD}
@@ -1405,6 +1434,12 @@ is_occupant_or_admin(JID, StateData) ->
_ -> false
end.
+%% Check if the user is an admin or owner.
+-spec is_admin(jid(), state()) -> boolean().
+is_admin(JID, StateData) ->
+ FAffiliation = get_affiliation(JID, StateData),
+ FAffiliation == admin orelse FAffiliation == owner.
+
%% Decide the fate of the message and its sender
%% Returns: continue_delivery | forget_message | {expulse_sender, Reason}
-spec decide_fate_message(message(), jid(), state()) ->
@@ -1602,7 +1637,7 @@ do_get_affiliation_fallback(JID, StateData) ->
-spec get_affiliations(state()) -> affiliations().
get_affiliations(#state{config = #config{persistent = false}} = StateData) ->
- get_affiliations_callback(StateData);
+ get_affiliations_fallback(StateData);
get_affiliations(StateData) ->
Room = StateData#state.room,
Host = StateData#state.host,
@@ -1610,13 +1645,13 @@ get_affiliations(StateData) ->
Mod = gen_mod:db_mod(ServerHost, mod_muc),
case Mod:get_affiliations(ServerHost, Room, Host) of
{error, _} ->
- get_affiliations_callback(StateData);
+ get_affiliations_fallback(StateData);
{ok, Affiliations} ->
Affiliations
end.
--spec get_affiliations_callback(state()) -> affiliations().
-get_affiliations_callback(StateData) ->
+-spec get_affiliations_fallback(state()) -> affiliations().
+get_affiliations_fallback(StateData) ->
StateData#state.affiliations.
-spec get_service_affiliation(jid(), state()) -> owner | none.
@@ -1935,7 +1970,7 @@ filter_presence(Presence) ->
XMLNS = xmpp:get_ns(El),
case catch binary:part(XMLNS, 0, size(?NS_MUC)) of
?NS_MUC -> false;
- _ -> true
+ _ -> XMLNS /= ?NS_HATS
end
end, xmpp:get_els(Presence)),
xmpp:set_els(Presence, Els).
@@ -2485,9 +2520,10 @@ send_new_presence(NJID, Reason, IsInitialPresence, StateData, OldStateData) ->
Pres = if Presence == undefined -> #presence{};
true -> Presence
end,
- Packet = xmpp:set_subtag(
- Pres, #muc_user{items = [Item],
- status_codes = StatusCodes}),
+ Packet = xmpp:set_subtag(
+ add_presence_hats(NJID, Pres, StateData),
+ #muc_user{items = [Item],
+ status_codes = StatusCodes}),
send_wrapped(jid:replace_resource(StateData#state.jid, Nick),
Info#user.jid, Packet, Node1, StateData),
Type = xmpp:get_type(Packet),
@@ -2536,7 +2572,9 @@ send_existing_presences1(ToJID, StateData) ->
false -> Item0
end,
Packet = xmpp:set_subtag(
- Presence, #muc_user{items = [Item]}),
+ add_presence_hats(
+ FromJID, Presence, StateData),
+ #muc_user{items = [Item]}),
send_wrapped(jid:replace_resource(StateData#state.jid, FromNick),
RealToJID, Packet, ?NS_MUCSUB_NODES_PRESENCE, StateData)
end
@@ -3579,7 +3617,8 @@ get_config(Lang, StateData, From) ->
{allow_voice_requests, Config#config.allow_voice_requests},
{allow_subscription, Config#config.allow_subscription},
{voice_request_min_interval, Config#config.voice_request_min_interval},
- {pubsub, Config#config.pubsub}]
+ {pubsub, Config#config.pubsub},
+ {enable_hats, Config#config.enable_hats}]
++
case ejabberd_captcha:is_feature_available() of
true ->
@@ -3667,6 +3706,7 @@ set_config(Opts, Config, ServerHost, Lang) ->
({maxusers, V}, C) -> C#config{max_users = V};
({enablelogging, V}, C) -> C#config{logging = V};
({pubsub, V}, C) -> C#config{pubsub = V};
+ ({enable_hats, V}, C) -> C#config{enable_hats = V};
({lang, L}, C) -> C#config{lang = L};
({captcha_whitelist, Js}, C) ->
LJIDs = [jid:tolower(J) || J <- Js],
@@ -3897,6 +3937,9 @@ set_opts([{Opt, Val} | Opts], StateData) ->
allow_subscription ->
StateData#state{config =
(StateData#state.config)#config{allow_subscription = Val}};
+ enable_hats ->
+ StateData#state{config =
+ (StateData#state.config)#config{enable_hats = Val}};
lang ->
StateData#state{config =
(StateData#state.config)#config{lang = Val}};
@@ -3927,6 +3970,11 @@ set_opts([{Opt, Val} | Opts], StateData) ->
end,
StateData#state{subject = Subj};
subject_author -> StateData#state{subject_author = Val};
+ hats_users ->
+ Hats = maps:from_list(
+ lists:map(fun({U, H}) -> {U, maps:from_list(H)} end,
+ Val)),
+ StateData#state{hats_users = Hats};
_ -> StateData
end,
set_opts(Opts, NSD).
@@ -3983,6 +4031,7 @@ make_opts(StateData) ->
?MAKE_CONFIG_OPT(#config.vcard),
?MAKE_CONFIG_OPT(#config.vcard_xupdate),
?MAKE_CONFIG_OPT(#config.pubsub),
+ ?MAKE_CONFIG_OPT(#config.enable_hats),
?MAKE_CONFIG_OPT(#config.lang),
{captcha_whitelist,
(?SETS):to_list((StateData#state.config)#config.captcha_whitelist)},
@@ -3990,6 +4039,9 @@ make_opts(StateData) ->
maps:to_list(StateData#state.affiliations)},
{subject, StateData#state.subject},
{subject_author, StateData#state.subject_author},
+ {hats_users,
+ lists:map(fun({U, H}) -> {U, maps:to_list(H)} end,
+ maps:to_list(StateData#state.hats_users))},
{hibernation_time, erlang:system_time(microsecond)},
{subscribers, Subscribers}].
@@ -4080,6 +4132,7 @@ maybe_forget_room(StateData) ->
make_disco_info(_From, StateData) ->
Config = StateData#state.config,
Feats = [?NS_VCARD, ?NS_MUC, ?NS_DISCO_INFO, ?NS_DISCO_ITEMS,
+ ?NS_COMMANDS,
?CONFIG_OPT_TO_FEATURE((Config#config.public),
<<"muc_public">>, <<"muc_hidden">>),
?CONFIG_OPT_TO_FEATURE((Config#config.persistent),
@@ -4120,6 +4173,77 @@ process_iq_disco_info(From, #iq{type = get, lang = Lang,
Extras = iq_disco_info_extras(Lang, StateData, false),
{result, DiscoInfo#disco_info{xdata = [Extras]}};
process_iq_disco_info(From, #iq{type = get, lang = Lang,
+ sub_els = [#disco_info{node = ?NS_COMMANDS}]},
+ StateData) ->
+ case (StateData#state.config)#config.enable_hats andalso
+ is_admin(From, StateData)
+ of
+ true ->
+ {result,
+ #disco_info{
+ identities = [#identity{category = <<"automation">>,
+ type = <<"command-list">>,
+ name = translate:translate(
+ Lang, ?T("Commands"))}]}};
+ false ->
+ Txt = ?T("Node not found"),
+ {error, xmpp:err_item_not_found(Txt, Lang)}
+ end;
+process_iq_disco_info(From, #iq{type = get, lang = Lang,
+ sub_els = [#disco_info{node = ?MUC_HAT_ADD_CMD}]},
+ StateData) ->
+ case (StateData#state.config)#config.enable_hats andalso
+ is_admin(From, StateData)
+ of
+ true ->
+ {result,
+ #disco_info{
+ identities = [#identity{category = <<"automation">>,
+ type = <<"command-node">>,
+ name = translate:translate(
+ Lang, ?T("Add a hat to a user"))}],
+ features = [?NS_COMMANDS]}};
+ false ->
+ Txt = ?T("Node not found"),
+ {error, xmpp:err_item_not_found(Txt, Lang)}
+ end;
+process_iq_disco_info(From, #iq{type = get, lang = Lang,
+ sub_els = [#disco_info{node = ?MUC_HAT_REMOVE_CMD}]},
+ StateData) ->
+ case (StateData#state.config)#config.enable_hats andalso
+ is_admin(From, StateData)
+ of
+ true ->
+ {result,
+ #disco_info{
+ identities = [#identity{category = <<"automation">>,
+ type = <<"command-node">>,
+ name = translate:translate(
+ Lang, ?T("Remove a hat from a user"))}],
+ features = [?NS_COMMANDS]}};
+ false ->
+ Txt = ?T("Node not found"),
+ {error, xmpp:err_item_not_found(Txt, Lang)}
+ end;
+process_iq_disco_info(From, #iq{type = get, lang = Lang,
+ sub_els = [#disco_info{node = ?MUC_HAT_LIST_CMD}]},
+ StateData) ->
+ case (StateData#state.config)#config.enable_hats andalso
+ is_admin(From, StateData)
+ of
+ true ->
+ {result,
+ #disco_info{
+ identities = [#identity{category = <<"automation">>,
+ type = <<"command-node">>,
+ name = translate:translate(
+ Lang, ?T("List users with hats"))}],
+ features = [?NS_COMMANDS]}};
+ false ->
+ Txt = ?T("Node not found"),
+ {error, xmpp:err_item_not_found(Txt, Lang)}
+ end;
+process_iq_disco_info(From, #iq{type = get, lang = Lang,
sub_els = [#disco_info{node = Node}]},
StateData) ->
try
@@ -4199,6 +4323,46 @@ process_iq_disco_items(From, #iq{type = get, sub_els = [#disco_items{node = <<>>
{result, #disco_items{}}
end
end;
+process_iq_disco_items(From, #iq{type = get, lang = Lang,
+ sub_els = [#disco_items{node = ?NS_COMMANDS}]},
+ StateData) ->
+ case (StateData#state.config)#config.enable_hats andalso
+ is_admin(From, StateData)
+ of
+ true ->
+ {result,
+ #disco_items{
+ items = [#disco_item{jid = StateData#state.jid,
+ node = ?MUC_HAT_ADD_CMD,
+ name = translate:translate(
+ Lang, ?T("Add a hat to a user"))},
+ #disco_item{jid = StateData#state.jid,
+ node = ?MUC_HAT_REMOVE_CMD,
+ name = translate:translate(
+ Lang, ?T("Remove a hat from a user"))},
+ #disco_item{jid = StateData#state.jid,
+ node = ?MUC_HAT_LIST_CMD,
+ name = translate:translate(
+ Lang, ?T("List users with hats"))}]}};
+ false ->
+ Txt = ?T("Node not found"),
+ {error, xmpp:err_item_not_found(Txt, Lang)}
+ end;
+process_iq_disco_items(From, #iq{type = get, lang = Lang,
+ sub_els = [#disco_items{node = Node}]},
+ StateData)
+ when Node == ?MUC_HAT_ADD_CMD;
+ Node == ?MUC_HAT_REMOVE_CMD;
+ Node == ?MUC_HAT_LIST_CMD ->
+ case (StateData#state.config)#config.enable_hats andalso
+ is_admin(From, StateData)
+ of
+ true ->
+ {result, #disco_items{}};
+ false ->
+ Txt = ?T("Node not found"),
+ {error, xmpp:err_item_not_found(Txt, Lang)}
+ end;
process_iq_disco_items(_From, #iq{lang = Lang}, _StateData) ->
Txt = ?T("Node not found"),
{error, xmpp:err_item_not_found(Txt, Lang)}.
@@ -4441,6 +4605,271 @@ get_mucroom_disco_items(StateData) ->
end, [], StateData#state.nicks),
#disco_items{items = Items}.
+-spec process_iq_adhoc(jid(), iq(), state()) ->
+ {result, adhoc_command()} |
+ {result, adhoc_command(), state()} |
+ {error, stanza_error()}.
+process_iq_adhoc(_From, #iq{type = get}, _StateData) ->
+ {error, xmpp:err_bad_request()};
+process_iq_adhoc(From, #iq{type = set, lang = Lang1,
+ sub_els = [#adhoc_command{} = Request]},
+ StateData) ->
+ % Ad-Hoc Commands are used only for Hats here
+ case (StateData#state.config)#config.enable_hats andalso
+ is_admin(From, StateData)
+ of
+ true ->
+ #adhoc_command{lang = Lang2, node = Node,
+ action = Action, xdata = XData} = Request,
+ Lang = case Lang2 of
+ <<"">> -> Lang1;
+ _ -> Lang2
+ end,
+ case {Node, Action} of
+ {_, cancel} ->
+ {result,
+ xmpp_util:make_adhoc_response(
+ Request,
+ #adhoc_command{status = canceled, lang = Lang,
+ node = Node})};
+ {?MUC_HAT_ADD_CMD, execute} ->
+ Form =
+ #xdata{
+ title = translate:translate(
+ Lang, ?T("Add a hat to a user")),
+ type = form,
+ fields =
+ [#xdata_field{
+ type = 'jid-single',
+ label = translate:translate(Lang, ?T("Jabber ID")),
+ required = true,
+ var = <<"jid">>},
+ #xdata_field{
+ type = 'text-single',
+ label = translate:translate(Lang, ?T("Hat title")),
+ var = <<"hat_title">>},
+ #xdata_field{
+ type = 'text-single',
+ label = translate:translate(Lang, ?T("Hat URI")),
+ required = true,
+ var = <<"hat_uri">>}
+ ]},
+ {result,
+ xmpp_util:make_adhoc_response(
+ Request,
+ #adhoc_command{
+ status = executing,
+ xdata = Form})};
+ {?MUC_HAT_ADD_CMD, complete} when XData /= undefined ->
+ JID = try
+ jid:decode(hd(xmpp_util:get_xdata_values(
+ <<"jid">>, XData)))
+ catch _:_ -> error
+ end,
+ URI = try
+ hd(xmpp_util:get_xdata_values(
+ <<"hat_uri">>, XData))
+ catch _:_ -> error
+ end,
+ Title = case xmpp_util:get_xdata_values(
+ <<"hat_title">>, XData) of
+ [] -> <<"">>;
+ [T] -> T
+ end,
+ if
+ (JID /= error) and (URI /= error) ->
+ case add_hat(JID, URI, Title, StateData) of
+ {ok, NewStateData} ->
+ store_room(NewStateData),
+ send_update_presence(
+ JID, NewStateData, StateData),
+ {result,
+ xmpp_util:make_adhoc_response(
+ Request,
+ #adhoc_command{status = completed}),
+ NewStateData};
+ {error, size_limit} ->
+ Txt = ?T("Hats limit exceeded"),
+ {error, xmpp:err_not_allowed(Txt, Lang)}
+ end;
+ true ->
+ {error, xmpp:err_bad_request()}
+ end;
+ {?MUC_HAT_ADD_CMD, complete} ->
+ {error, xmpp:err_bad_request()};
+ {?MUC_HAT_ADD_CMD, _} ->
+ Txt = ?T("Incorrect value of 'action' attribute"),
+ {error, xmpp:err_bad_request(Txt, Lang)};
+ {?MUC_HAT_REMOVE_CMD, execute} ->
+ Form =
+ #xdata{
+ title = translate:translate(
+ Lang, ?T("Remove a hat from a user")),
+ type = form,
+ fields =
+ [#xdata_field{
+ type = 'jid-single',
+ label = translate:translate(Lang, ?T("Jabber ID")),
+ required = true,
+ var = <<"jid">>},
+ #xdata_field{
+ type = 'text-single',
+ label = translate:translate(Lang, ?T("Hat URI")),
+ required = true,
+ var = <<"hat_uri">>}
+ ]},
+ {result,
+ xmpp_util:make_adhoc_response(
+ Request,
+ #adhoc_command{
+ status = executing,
+ xdata = Form})};
+ {?MUC_HAT_REMOVE_CMD, complete} when XData /= undefined ->
+ JID = try
+ jid:decode(hd(xmpp_util:get_xdata_values(
+ <<"jid">>, XData)))
+ catch _:_ -> error
+ end,
+ URI = try
+ hd(xmpp_util:get_xdata_values(
+ <<"hat_uri">>, XData))
+ catch _:_ -> error
+ end,
+ if
+ (JID /= error) and (URI /= error) ->
+ NewStateData = del_hat(JID, URI, StateData),
+ store_room(NewStateData),
+ send_update_presence(
+ JID, NewStateData, StateData),
+ {result,
+ xmpp_util:make_adhoc_response(
+ Request,
+ #adhoc_command{status = completed}),
+ NewStateData};
+ true ->
+ {error, xmpp:err_bad_request()}
+ end;
+ {?MUC_HAT_REMOVE_CMD, complete} ->
+ {error, xmpp:err_bad_request()};
+ {?MUC_HAT_REMOVE_CMD, _} ->
+ Txt = ?T("Incorrect value of 'action' attribute"),
+ {error, xmpp:err_bad_request(Txt, Lang)};
+ {?MUC_HAT_LIST_CMD, execute} ->
+ Hats = get_all_hats(StateData),
+ Items =
+ lists:map(
+ fun({JID, URI, Title}) ->
+ [#xdata_field{
+ var = <<"jid">>,
+ values = [jid:encode(JID)]},
+ #xdata_field{
+ var = <<"hat_title">>,
+ values = [URI]},
+ #xdata_field{
+ var = <<"hat_uri">>,
+ values = [Title]}]
+ end, Hats),
+ Form =
+ #xdata{
+ title = translate:translate(
+ Lang, ?T("List of users with hats")),
+ type = result,
+ reported =
+ [#xdata_field{
+ label = translate:translate(Lang, ?T("Jabber ID")),
+ var = <<"jid">>},
+ #xdata_field{
+ label = translate:translate(Lang, ?T("Hat title")),
+ var = <<"hat_title">>},
+ #xdata_field{
+ label = translate:translate(Lang, ?T("Hat URI")),
+ var = <<"hat_uri">>}],
+ items = Items},
+ {result,
+ xmpp_util:make_adhoc_response(
+ Request,
+ #adhoc_command{
+ status = completed,
+ xdata = Form})};
+ {?MUC_HAT_LIST_CMD, _} ->
+ Txt = ?T("Incorrect value of 'action' attribute"),
+ {error, xmpp:err_bad_request(Txt, Lang)};
+ _ ->
+ {error, xmpp:err_item_not_found()}
+ end;
+ _ ->
+ {error, xmpp:err_forbidden()}
+ end.
+
+-spec add_hat(jid(), binary(), binary(), state()) ->
+ {ok, state()} | {error, size_limit}.
+add_hat(JID, URI, Title, StateData) ->
+ Hats = StateData#state.hats_users,
+ LJID = jid:remove_resource(jid:tolower(JID)),
+ UserHats = maps:get(LJID, Hats, #{}),
+ UserHats2 = maps:put(URI, Title, UserHats),
+ USize = maps:size(UserHats2),
+ if
+ USize =< ?MAX_HATS_PER_USER ->
+ Hats2 = maps:put(LJID, UserHats2, Hats),
+ Size = maps:size(Hats2),
+ if
+ Size =< ?MAX_HATS_USERS ->
+ {ok, StateData#state{hats_users = Hats2}};
+ true ->
+ {error, size_limit}
+ end;
+ true ->
+ {error, size_limit}
+ end.
+
+-spec del_hat(jid(), binary(), state()) -> state().
+del_hat(JID, URI, StateData) ->
+ Hats = StateData#state.hats_users,
+ LJID = jid:remove_resource(jid:tolower(JID)),
+ UserHats = maps:get(LJID, Hats, #{}),
+ UserHats2 = maps:remove(URI, UserHats),
+ Hats2 =
+ case maps:size(UserHats2) of
+ 0 ->
+ maps:remove(LJID, Hats);
+ _ ->
+ maps:put(LJID, UserHats2, Hats)
+ end,
+ StateData#state{hats_users = Hats2}.
+
+-spec get_all_hats(state()) -> list({jid(), binary(), binary()}).
+get_all_hats(StateData) ->
+ lists:flatmap(
+ fun({LJID, H}) ->
+ JID = jid:make(LJID),
+ lists:map(fun({URI, Title}) -> {JID, URI, Title} end,
+ maps:to_list(H))
+ end,
+ maps:to_list(StateData#state.hats_users)).
+
+-spec add_presence_hats(jid(), #presence{}, state()) -> #presence{}.
+add_presence_hats(JID, Pres, StateData) ->
+ case (StateData#state.config)#config.enable_hats of
+ true ->
+ Hats = StateData#state.hats_users,
+ LJID = jid:remove_resource(jid:tolower(JID)),
+ UserHats = maps:get(LJID, Hats, #{}),
+ case maps:size(UserHats) of
+ 0 -> Pres;
+ _ ->
+ Items =
+ lists:map(fun({URI, Title}) ->
+ #muc_hat{uri = URI, title = Title}
+ end,
+ maps:to_list(UserHats)),
+ xmpp:set_subtag(Pres,
+ #muc_hats{hats = Items})
+ end;
+ false ->
+ Pres
+ end.
+
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
% Voice request support
@@ -4687,7 +5116,7 @@ send_subscriptions_change_notifications(From, Nick, Type, State) ->
id = p1_rand:get_string(),
sub_els = [Payload1]}]}}]},
ejabberd_router_multicast:route_multicast(State#state.jid, State#state.server_host,
- WJ, Packet1, true);
+ WJ, Packet1, false);
true -> ok
end,
if WN /= [] ->
@@ -4703,7 +5132,7 @@ send_subscriptions_change_notifications(From, Nick, Type, State) ->
id = p1_rand:get_string(),
sub_els = [Payload2]}]}}]},
ejabberd_router_multicast:route_multicast(State#state.jid, State#state.server_host,
- WN, Packet2, true);
+ WN, Packet2, false);
true -> ok
end.
@@ -4927,6 +5356,23 @@ muc_subscribers_put(Subscriber, MUCSubscribers) ->
subscriber_nodes = NewSubNodes}.
+cleanup_affiliations(State) ->
+ case mod_muc_opt:cleanup_affiliations_on_start(State#state.server_host) of
+ true ->
+ Affiliations =
+ maps:filter(
+ fun({LUser, LServer, _}, _) ->
+ case ejabberd_router:is_my_host(LServer) of
+ true ->
+ ejabberd_auth:user_exists(LUser, LServer);
+ false ->
+ true
+ end
+ end, State#state.affiliations),
+ State#state{affiliations = Affiliations};
+ false ->
+ State
+ end.
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%% Detect messange stanzas that don't have meaningful content
diff --git a/src/mod_muc_sql.erl b/src/mod_muc_sql.erl
index 1310cde7b..8aa7ad62b 100644
--- a/src/mod_muc_sql.erl
+++ b/src/mod_muc_sql.erl
@@ -38,7 +38,7 @@
register_online_user/4, unregister_online_user/4,
count_online_rooms_by_user/3, get_online_rooms_by_user/3,
get_subscribed_rooms/3, get_rooms_without_subscribers/2,
- find_online_room_by_pid/2]).
+ find_online_room_by_pid/2, remove_user/2]).
-export([set_affiliation/6, set_affiliations/4, get_affiliation/5,
get_affiliations/3, search_affiliation/4]).
@@ -465,6 +465,13 @@ get_subscribed_rooms(LServer, Host, Jid) ->
{error, db_failure}
end.
+remove_user(LUser, LServer) ->
+ SJID = jid:encode(jid:make(LUser, LServer)),
+ ejabberd_sql:sql_query(
+ LServer,
+ ?SQL("delete from muc_room_subscribers where jid=%(SJID)s")),
+ ok.
+
%%%===================================================================
%%% Internal functions
%%%===================================================================
diff --git a/src/mod_multicast.erl b/src/mod_multicast.erl
index 161d3a4c4..fa076da70 100644
--- a/src/mod_multicast.erl
+++ b/src/mod_multicast.erl
@@ -35,7 +35,7 @@
%% API
-export([start/2, stop/1, reload/3,
- user_send_packet/1]).
+ user_send_packet/1]).
%% gen_server callbacks
-export([init/1, handle_info/2, handle_call/3,
@@ -51,11 +51,6 @@
response,
ts :: integer()}).
--record(dest, {jid_string :: binary() | none,
- jid_jid :: jid() | undefined,
- type :: bcc | cc | noreply | ofrom | replyroom | replyto | to,
- address :: address()}).
-
-type limit_value() :: {default | custom, integer()}.
-record(limits, {message :: limit_value(),
presence :: limit_value()}).
@@ -63,14 +58,6 @@
-record(service_limits, {local :: #limits{},
remote :: #limits{}}).
--type routing() :: route_single | {route_multicast, binary(), #service_limits{}}.
-
--record(group, {server :: binary(),
- dests :: [#dest{}],
- multicast :: routing() | undefined,
- others :: [address()],
- addresses :: [address()]}).
-
-record(state, {lserver :: binary(),
lservice :: binary(),
access :: atom(),
@@ -117,7 +104,7 @@ reload(LServerS, NewOpts, OldOpts) ->
user_send_packet({#presence{} = Packet, C2SState} = Acc) ->
case xmpp:get_subtag(Packet, #addresses{}) of
#addresses{list = Addresses} ->
- {ToDeliver, _Delivereds} = split_addresses_todeliver(Addresses),
+ {CC, BCC, _Invalid, _Delivered} = partition_addresses(Addresses),
NewState =
lists:foldl(
fun(Address, St) ->
@@ -138,7 +125,7 @@ user_send_packet({#presence{} = Packet, C2SState} = Acc) ->
undefined ->
St
end
- end, C2SState, ToDeliver),
+ end, C2SState, CC ++ BCC),
{Packet, NewState};
false ->
Acc
@@ -308,19 +295,10 @@ iq_vcard(Lang, State) ->
%%%-------------------------
-spec route_trusted(binary(), binary(), jid(), [jid()], stanza()) -> 'ok'.
-route_trusted(LServiceS, LServerS, FromJID,
- Destinations, Packet) ->
- Packet_stripped = Packet,
- Delivereds = [],
- Dests2 = lists:map(
- fun(D) ->
- #dest{jid_string = jid:encode(D),
- jid_jid = D, type = bcc,
- address = #address{type = bcc, jid = D}}
- end, Destinations),
- Groups = group_dests(Dests2),
- route_common(LServerS, LServiceS, FromJID, Groups,
- Delivereds, Packet_stripped).
+route_trusted(LServiceS, LServerS, FromJID, Destinations, Packet) ->
+ Addresses = [#address{type = bcc, jid = D} || D <- Destinations],
+ Groups = group_by_destinations(Addresses, #{}),
+ route_grouped(LServerS, LServiceS, FromJID, Groups, [], Packet).
-spec route_untrusted(binary(), binary(), atom(), #service_limits{}, stanza()) -> 'ok'.
route_untrusted(LServiceS, LServerS, Access, SLimits, Packet) ->
@@ -356,50 +334,88 @@ route_untrusted(LServiceS, LServerS, Access, SLimits, Packet) ->
route_untrusted2(LServiceS, LServerS, Access, SLimits, Packet) ->
FromJID = xmpp:get_from(Packet),
ok = check_access(LServerS, Access, FromJID),
- {ok, Packet_stripped, Addresses} = strip_addresses_element(Packet),
- {To_deliver, Delivereds} = split_addresses_todeliver(Addresses),
- Dests = convert_dest_record(To_deliver),
- {Dests2, Not_jids} = split_dests_jid(Dests),
- report_not_jid(FromJID, Packet, Not_jids),
- ok = check_limit_dests(SLimits, FromJID, Packet, Dests2),
- Groups = group_dests(Dests2),
+ {ok, PacketStripped, Addresses} = strip_addresses_element(Packet),
+ {CC, BCC, NotJids, Rest} = partition_addresses(Addresses),
+ report_not_jid(FromJID, Packet, NotJids),
+ ok = check_limit_dests(SLimits, FromJID, Packet, length(CC) + length(BCC)),
+ Groups0 = group_by_destinations(CC, #{}),
+ Groups = group_by_destinations(BCC, Groups0),
ok = check_relay(FromJID#jid.server, LServerS, Groups),
- route_common(LServerS, LServiceS, FromJID, Groups,
- Delivereds, Packet_stripped).
-
--spec route_common(binary(), binary(), jid(), [#group{}],
- [address()], stanza()) -> 'ok'.
-route_common(LServerS, LServiceS, FromJID, Groups,
- Delivereds, Packet_stripped) ->
- Groups2 = look_cached_servers(LServerS, LServiceS, Groups),
- Groups3 = build_others_xml(Groups2),
- Groups4 = add_addresses(Delivereds, Groups3),
- AGroups = decide_action_groups(Groups4),
- act_groups(FromJID, Packet_stripped, LServiceS,
- AGroups).
-
--spec act_groups(jid(), stanza(), binary(), [{routing(), #group{}}]) -> 'ok'.
-act_groups(FromJID, Packet_stripped, LServiceS, AGroups) ->
+ route_grouped(LServerS, LServiceS, FromJID, Groups, Rest, PacketStripped).
+
+-spec mark_as_delivered([address()]) -> [address()].
+mark_as_delivered(Addresses) ->
+ [A#address{delivered = true} || A <- Addresses].
+
+-spec route_individual(jid(), [address()], [address()], [address()], stanza()) -> ok.
+route_individual(From, CC, BCC, Other, Packet) ->
+ CCDelivered = mark_as_delivered(CC),
+ Addresses = CCDelivered ++ Other,
+ PacketWithAddresses = xmpp:append_subtags(Packet, [#addresses{list = Addresses}]),
lists:foreach(
- fun(AGroup) ->
- perform(FromJID, Packet_stripped, LServiceS,
- AGroup)
- end, AGroups).
-
--spec perform(jid(), stanza(), binary(),
- {routing(), #group{}}) -> 'ok'.
-perform(From, Packet, _,
- {route_single, Group}) ->
+ fun(#address{jid = To}) ->
+ ejabberd_router:route(xmpp:set_from_to(PacketWithAddresses, From, To))
+ end, CC),
lists:foreach(
- fun(ToUser) ->
- Group_others = strip_other_bcc(ToUser, Group#group.others),
- route_packet(From, ToUser, Packet,
- Group_others, Group#group.addresses)
- end, Group#group.dests);
-perform(From, Packet, _,
- {{route_multicast, JID, RLimits}, Group}) ->
- route_packet_multicast(From, JID, Packet,
- Group#group.dests, Group#group.addresses, RLimits).
+ fun(#address{jid = To} = Address) ->
+ Packet2 = case Addresses of
+ [] ->
+ Packet;
+ _ ->
+ xmpp:append_subtags(Packet, [#addresses{list = [Address | Addresses]}])
+ end,
+ ejabberd_router:route(xmpp:set_from_to(Packet2, From, To))
+ end, BCC).
+
+-spec route_chunk(jid(), jid(), stanza(), [address()]) -> ok.
+route_chunk(From, To, Packet, Addresses) ->
+ PacketWithAddresses = xmpp:append_subtags(Packet, [#addresses{list = Addresses}]),
+ ejabberd_router:route(xmpp:set_from_to(PacketWithAddresses, From, To)).
+
+-spec route_in_chunks(jid(), jid(), stanza(), integer(), [address()], [address()], [address()]) -> ok.
+route_in_chunks(_From, _To, _Packet, _Limit, [], [], _) ->
+ ok;
+route_in_chunks(From, To, Packet, Limit, CC, BCC, RestOfAddresses) when length(CC) > Limit ->
+ {Chunk, Rest} = lists:split(Limit, CC),
+ route_chunk(From, To, Packet, Chunk ++ RestOfAddresses),
+ route_in_chunks(From, To, Packet, Limit, Rest, BCC, RestOfAddresses);
+route_in_chunks(From, To, Packet, Limit, [], BCC, RestOfAddresses) when length(BCC) > Limit ->
+ {Chunk, Rest} = lists:split(Limit, BCC),
+ route_chunk(From, To, Packet, Chunk ++ RestOfAddresses),
+ route_in_chunks(From, To, Packet, Limit, [], Rest, RestOfAddresses);
+route_in_chunks(From, To, Packet, Limit, CC, BCC, RestOfAddresses) when length(BCC) + length(CC) > Limit ->
+ {Chunk, Rest} = lists:split(Limit - length(CC), BCC),
+ route_chunk(From, To, Packet, CC ++ Chunk ++ RestOfAddresses),
+ route_in_chunks(From, To, Packet, Limit, [], Rest, RestOfAddresses);
+route_in_chunks(From, To, Packet, _Limit, CC, BCC, RestOfAddresses) ->
+ route_chunk(From, To, Packet, CC ++ BCC ++ RestOfAddresses).
+
+-spec route_multicast(jid(), jid(), [address()], [address()], [address()], stanza(), #limits{}) -> ok.
+route_multicast(From, To, CC, BCC, RestOfAddresses, Packet, Limits) ->
+ {_Type, Limit} = get_limit_number(element(1, Packet),
+ Limits),
+ route_in_chunks(From, To, Packet, Limit, CC, BCC, RestOfAddresses).
+
+-spec route_grouped(binary(), binary(), jid(), #{}, [address()], stanza()) -> ok.
+route_grouped(LServer, LService, From, Groups, RestOfAddresses, Packet) ->
+ maps:fold(
+ fun(Server, {CC, BCC}, _) ->
+ OtherCC = maps:fold(
+ fun(Server2, _, Res) when Server2 == Server ->
+ Res;
+ (_, {CC2, _}, Res) ->
+ mark_as_delivered(CC2) ++ Res
+ end, [], Groups),
+ case search_server_on_cache(Server,
+ LServer, LService,
+ {?MAXTIME_CACHE_POSITIVE,
+ ?MAXTIME_CACHE_NEGATIVE}) of
+ route_single ->
+ route_individual(From, CC, BCC, OtherCC ++ RestOfAddresses, Packet);
+ {route_multicast, Service, Limits} ->
+ route_multicast(From, Service, CC, BCC, OtherCC ++ RestOfAddresses, Packet, Limits)
+ end
+ end, ok, Groups).
%%%-------------------------
%%% Check access permission
@@ -426,244 +442,88 @@ strip_addresses_element(Packet) ->
end.
%%%-------------------------
-%%% Strip third-party bcc 'addresses'
-%%%-------------------------
-
-strip_other_bcc(#dest{jid_jid = ToUserJid}, Group_others) ->
- lists:filter(
- fun(#address{jid = JID, type = Type}) ->
- case {JID, Type} of
- {ToUserJid, bcc} -> true;
- {_, bcc} -> false;
- _ -> true
- end
- end,
- Group_others).
-
-%%%-------------------------
%%% Split Addresses
%%%-------------------------
--spec split_addresses_todeliver([address()]) -> {[address()], [address()]}.
-split_addresses_todeliver(Addresses) ->
- lists:partition(
- fun(#address{delivered = true}) ->
- false;
- (#address{type = Type}) ->
- case Type of
- to -> true;
- cc -> true;
- bcc -> true;
- _ -> false
- end
- end, Addresses).
+partition_addresses(Addresses) ->
+ lists:foldl(
+ fun(#address{delivered = true} = A, {C, B, I, D}) ->
+ {C, B, I, [A | D]};
+ (#address{type = T, jid = undefined} = A, {C, B, I, D})
+ when T == to; T == cc; T == bcc ->
+ {C, B, [A | I], D};
+ (#address{type = T} = A, {C, B, I, D})
+ when T == to; T == cc ->
+ {[A | C], B, I, D};
+ (#address{type = bcc} = A, {C, B, I, D}) ->
+ {C, [A | B], I, D};
+ (A, {C, B, I, D}) ->
+ {C, B, I, [A | D]}
+ end, {[], [], [], []}, Addresses).
%%%-------------------------
%%% Check does not exceed limit of destinations
%%%-------------------------
--spec check_limit_dests(#service_limits{}, jid(), stanza(), [address()]) -> ok.
-check_limit_dests(SLimits, FromJID, Packet,
- Addresses) ->
+-spec check_limit_dests(#service_limits{}, jid(), stanza(), integer()) -> ok.
+check_limit_dests(SLimits, FromJID, Packet, NumOfAddresses) ->
SenderT = sender_type(FromJID),
Limits = get_slimit_group(SenderT, SLimits),
- Type_of_stanza = type_of_stanza(Packet),
- {_Type, Limit_number} = get_limit_number(Type_of_stanza,
- Limits),
- case length(Addresses) > Limit_number of
+ StanzaType = type_of_stanza(Packet),
+ {_Type, Limit} = get_limit_number(StanzaType,
+ Limits),
+ case NumOfAddresses > Limit of
false -> ok;
true -> throw(etoorec)
end.
-%%%-------------------------
-%%% Convert Destination XML to record
-%%%-------------------------
-
--spec convert_dest_record([address()]) -> [#dest{}].
-convert_dest_record(Addrs) ->
- lists:map(
- fun(#address{jid = undefined, type = Type} = Addr) ->
- #dest{jid_string = none,
- type = Type, address = Addr};
- (#address{jid = JID, type = Type} = Addr) ->
- #dest{jid_string = jid:encode(JID), jid_jid = JID,
- type = Type, address = Addr}
- end, Addrs).
-
-%%%-------------------------
-%%% Split destinations by existence of JID
-%%% and send error messages for other dests
-%%%-------------------------
--spec split_dests_jid([#dest{}]) -> {[#dest{}], [#dest{}]}.
-split_dests_jid(Dests) ->
- lists:partition(fun (Dest) ->
- case Dest#dest.jid_string of
- none -> false;
- _ -> true
- end
- end,
- Dests).
-
--spec report_not_jid(jid(), stanza(), [#dest{}]) -> any().
-report_not_jid(From, Packet, Dests) ->
- Dests2 = [fxml:element_to_binary(xmpp:encode(Dest#dest.address))
- || Dest <- Dests],
- [route_error(
- xmpp:set_from_to(Packet, From, From), jid_malformed,
- str:format(?T("This service can not process the address: ~s"), [D]))
- || D <- Dests2].
+-spec report_not_jid(jid(), stanza(), [address()]) -> any().
+report_not_jid(From, Packet, Addresses) ->
+ lists:foreach(
+ fun(Address) ->
+ route_error(
+ xmpp:set_from_to(Packet, From, From), jid_malformed,
+ str:format(?T("This service can not process the address: ~s"),
+ [fxml:element_to_binary(xmpp:encode(Address))]))
+ end, Addresses).
%%%-------------------------
%%% Group destinations by their servers
%%%-------------------------
--spec group_dests([#dest{}]) -> [#group{}].
-group_dests(Dests) ->
- D = lists:foldl(fun (Dest, Dict) ->
- ServerS = (Dest#dest.jid_jid)#jid.server,
- dict:append(ServerS, Dest, Dict)
- end,
- dict:new(), Dests),
- Keys = dict:fetch_keys(D),
- [#group{server = Key, dests = dict:fetch(Key, D),
- addresses = [], others = []}
- || Key <- Keys].
-
-%%%-------------------------
-%%% Look for cached responses
-%%%-------------------------
-
-look_cached_servers(LServerS, LServiceS, Groups) ->
- [look_cached(LServerS, LServiceS, Group) || Group <- Groups].
-
-look_cached(LServerS, LServiceS, G) ->
- Maxtime_positive = (?MAXTIME_CACHE_POSITIVE),
- Maxtime_negative = (?MAXTIME_CACHE_NEGATIVE),
- Cached_response = search_server_on_cache(G#group.server,
- LServerS, LServiceS,
- {Maxtime_positive,
- Maxtime_negative}),
- G#group{multicast = Cached_response}.
-
-%%%-------------------------
-%%% Build delivered XML element
-%%%-------------------------
-
-build_others_xml(Groups) ->
- [Group#group{others =
- build_other_xml(Group#group.dests)}
- || Group <- Groups].
-
-build_other_xml(Dests) ->
- lists:foldl(fun (Dest, R) ->
- XML = Dest#dest.address,
- case Dest#dest.type of
- to -> [add_delivered(XML) | R];
- cc -> [add_delivered(XML) | R];
- _ -> [XML | R]
- end
- end,
- [], Dests).
-
--spec add_delivered(address()) -> address().
-add_delivered(Addr) ->
- Addr#address{delivered = true}.
-
-%%%-------------------------
-%%% Add preliminary packets
-%%%-------------------------
-
-add_addresses(Delivereds, Groups) ->
- Ps = [Group#group.others || Group <- Groups],
- add_addresses2(Delivereds, Groups, [], [], Ps).
-
-add_addresses2(_, [], Res, _, []) -> Res;
-add_addresses2(Delivereds, [Group | Groups], Res, Pa,
- [Pi | Pz]) ->
- Addresses = lists:append([Delivereds] ++ Pa ++ Pz),
- Group2 = Group#group{addresses = Addresses},
- add_addresses2(Delivereds, Groups, [Group2 | Res],
- [Pi | Pa], Pz).
-
-%%%-------------------------
-%%% Decide action groups
-%%%-------------------------
-
--spec decide_action_groups([#group{}]) -> [{routing(), #group{}}].
-decide_action_groups(Groups) ->
- [{Group#group.multicast, Group}
- || Group <- Groups].
+group_by_destinations(Addrs, Map) ->
+ lists:foldl(
+ fun
+ (#address{type = Type, jid = #jid{lserver = Server}} = Addr, Map2) when Type == to; Type == cc ->
+ maps:update_with(Server,
+ fun({CC, BCC}) ->
+ {[Addr | CC], BCC}
+ end, {[Addr], []}, Map2);
+ (#address{type = bcc, jid = #jid{lserver = Server}} = Addr, Map2) ->
+ maps:update_with(Server,
+ fun({CC, BCC}) ->
+ {CC, [Addr | BCC]}
+ end, {[], [Addr]}, Map2)
+ end, Map, Addrs).
%%%-------------------------
%%% Route packet
%%%-------------------------
--spec route_packet(jid(), #dest{}, stanza(), [addresses()], [addresses()]) -> 'ok'.
-route_packet(From, ToDest, Packet, Others, Addresses) ->
- Dests = case ToDest#dest.type of
- bcc -> [];
- _ -> [ToDest]
- end,
- route_packet2(From, ToDest#dest.jid_string, Dests,
- Packet, {Others, Addresses}).
-
--spec route_packet_multicast(jid(), binary(), stanza(), [#dest{}], [address()], #limits{}) -> 'ok'.
-route_packet_multicast(From, ToS, Packet, Dests,
- Addresses, Limits) ->
- Type_of_stanza = type_of_stanza(Packet),
- {_Type, Limit_number} = get_limit_number(Type_of_stanza,
- Limits),
- Fragmented_dests = fragment_dests(Dests, Limit_number),
- lists:foreach(fun(DFragment) ->
- route_packet2(From, ToS, DFragment, Packet,
- Addresses)
- end, Fragmented_dests).
-
--spec route_packet2(jid(), binary(), [#dest{}], stanza(), {[address()], [address()]} | [address()]) -> 'ok'.
-route_packet2(From, ToS, Dests, Packet, Addresses) ->
- Els = case append_dests(Dests, Addresses) of
- [] ->
- xmpp:get_els(Packet);
- ACs ->
- [#addresses{list = ACs}|xmpp:get_els(Packet)]
- end,
- Packet2 = xmpp:set_els(Packet, Els),
- ToJID = stj(ToS),
- ejabberd_router:route(xmpp:set_from_to(Packet2, From, ToJID)).
-
--spec append_dests([#dest{}], {[address()], [address()]} | [address()]) -> [address()].
-append_dests(_Dests, {Others, Addresses}) ->
- Addresses ++ Others;
-append_dests([], Addresses) -> Addresses;
-append_dests([Dest | Dests], Addresses) ->
- append_dests(Dests, [Dest#dest.address | Addresses]).
-
%%%-------------------------
%%% Check relay
%%%-------------------------
--spec check_relay(binary(), binary(), [#group{}]) -> ok.
+-spec check_relay(binary(), binary(), #{}) -> ok.
check_relay(RS, LS, Gs) ->
- case check_relay_required(RS, LS, Gs) of
- false -> ok;
- true -> throw(edrelay)
- end.
-
--spec check_relay_required(binary(), binary(), [#group{}]) -> boolean().
-check_relay_required(RServer, LServerS, Groups) ->
- case lists:suffix(str:tokens(LServerS, <<".">>),
- str:tokens(RServer, <<".">>)) of
- true -> false;
- false -> check_relay_required(LServerS, Groups)
+ case lists:suffix(str:tokens(LS, <<".">>),
+ str:tokens(RS, <<".">>)) orelse
+ (maps:is_key(LS, Gs) andalso maps:size(Gs) == 1) of
+ true -> ok;
+ _ -> throw(edrelay)
end.
--spec check_relay_required(binary(), [#group{}]) -> boolean().
-check_relay_required(LServerS, Groups) ->
- lists:any(fun (Group) -> Group#group.server /= LServerS
- end,
- Groups).
-
%%%-------------------------
%%% Check protocol support: Send request
%%%-------------------------
@@ -1060,20 +920,6 @@ get_slimit_group(local, SLimits) ->
get_slimit_group(remote, SLimits) ->
SLimits#service_limits.remote.
-fragment_dests(Dests, Limit_number) ->
- {R, _} = lists:foldl(fun (Dest, {Res, Count}) ->
- case Count of
- Limit_number ->
- Head2 = [Dest], {[Head2 | Res], 0};
- _ ->
- [Head | Tail] = Res,
- Head2 = [Dest | Head],
- {[Head2 | Tail], Count + 1}
- end
- end,
- {[[]], 0}, Dests),
- R.
-
%%%-------------------------
%%% Limits: XEP-0128 Service Discovery Extensions
%%%-------------------------
diff --git a/src/mod_pubsub.erl b/src/mod_pubsub.erl
index 2e40d8f0e..d161ec10c 100644
--- a/src/mod_pubsub.erl
+++ b/src/mod_pubsub.erl
@@ -95,7 +95,7 @@
terminate/2, code_change/3, depends/2, mod_opt_type/1, mod_options/1]).
%% ejabberd commands
--export([get_commands_spec/0, delete_old_items/1]).
+-export([get_commands_spec/0, delete_old_items/1, delete_expired_items/0]).
-export([route/1]).
@@ -3431,6 +3431,14 @@ max_items(Host, Options) ->
end
end.
+-spec item_expire(host(), [{atom(), any()}]) -> non_neg_integer() | infinity.
+item_expire(Host, Options) ->
+ case get_option(Options, item_expire) of
+ I when is_integer(I), I < 0 -> 0;
+ I when is_integer(I) -> I;
+ _ -> get_max_item_expire_node(Host)
+ end.
+
-spec get_configure_xfields(_, pubsub_node_config:result(),
binary(), [binary()]) -> [xdata_field()].
get_configure_xfields(_Type, Options, Lang, Groups) ->
@@ -3504,17 +3512,24 @@ decode_node_config(undefined, _, _) ->
decode_node_config(#xdata{fields = Fs}, Host, Lang) ->
try
Config = pubsub_node_config:decode(Fs),
- Max = get_max_items_node(Host),
- case {check_opt_range(max_items, Config, Max),
+ MaxItems = get_max_items_node(Host),
+ MaxExpiry = get_max_item_expire_node(Host),
+ case {check_opt_range(max_items, Config, MaxItems),
+ check_opt_range(item_expire, Config, MaxExpiry),
check_opt_range(max_payload_size, Config, ?MAX_PAYLOAD_SIZE)} of
- {true, true} ->
+ {true, true, true} ->
Config;
- {true, false} ->
+ {true, true, false} ->
erlang:error(
{pubsub_node_config,
{bad_var_value, <<"pubsub#max_payload_size">>,
?NS_PUBSUB_NODE_CONFIG}});
- {false, _} ->
+ {true, false, _} ->
+ erlang:error(
+ {pubsub_node_config,
+ {bad_var_value, <<"pubsub#item_expire">>,
+ ?NS_PUBSUB_NODE_CONFIG}});
+ {false, _, _} ->
erlang:error(
{pubsub_node_config,
{bad_var_value, <<"pubsub#max_items">>,
@@ -3560,20 +3575,24 @@ decode_get_pending(#xdata{fields = Fs}, Lang) ->
end.
-spec check_opt_range(atom(), [proplists:property()],
- non_neg_integer() | unlimited | undefined) -> boolean().
-check_opt_range(_Opt, _Opts, undefined) ->
- true;
+ non_neg_integer() | unlimited | infinity) -> boolean().
check_opt_range(_Opt, _Opts, unlimited) ->
true;
+check_opt_range(_Opt, _Opts, infinity) ->
+ true;
check_opt_range(Opt, Opts, Max) ->
case proplists:get_value(Opt, Opts, Max) of
max -> true;
Val -> Val =< Max
end.
--spec get_max_items_node(host()) -> undefined | unlimited | non_neg_integer().
+-spec get_max_items_node(host()) -> unlimited | non_neg_integer().
get_max_items_node(Host) ->
- config(Host, max_items_node, undefined).
+ config(Host, max_items_node, ?MAXITEMS).
+
+-spec get_max_item_expire_node(host()) -> infinity | non_neg_integer().
+get_max_item_expire_node(Host) ->
+ config(Host, max_item_expire_node, infinity).
-spec get_max_subscriptions_node(host()) -> undefined | non_neg_integer().
get_max_subscriptions_node(Host) ->
@@ -4181,16 +4200,63 @@ delete_old_items(N) ->
ok
end.
+-spec delete_expired_items() -> ok | error.
+delete_expired_items() ->
+ Results = lists:flatmap(
+ fun(Host) ->
+ case tree_action(Host, get_all_nodes, [Host]) of
+ Nodes when is_list(Nodes) ->
+ lists:map(
+ fun(#pubsub_node{id = Nidx, type = Type,
+ options = Options}) ->
+ case item_expire(Host, Options) of
+ infinity ->
+ ok;
+ Seconds ->
+ case node_action(
+ Host, Type,
+ remove_expired_items,
+ [Nidx, Seconds]) of
+ {result, []} ->
+ ok;
+ {result, [_|_]} ->
+ unset_cached_item(
+ Host, Nidx);
+ {error, _} ->
+ error
+ end
+ end
+ end, Nodes);
+ _ ->
+ error
+ end
+ end, ejabberd_option:hosts()),
+ case lists:member(error, Results) of
+ true ->
+ error;
+ false ->
+ ok
+ end.
+
-spec get_commands_spec() -> [ejabberd_commands()].
get_commands_spec() ->
[#ejabberd_commands{name = delete_old_pubsub_items, tags = [purge],
desc = "Keep only NUMBER of PubSub items per node",
+ note = "added in 21.12",
module = ?MODULE, function = delete_old_items,
args_desc = ["Number of items to keep per node"],
args = [{number, integer}],
result = {res, rescode},
result_desc = "0 if command failed, 1 when succeeded",
args_example = [1000],
+ result_example = ok},
+ #ejabberd_commands{name = delete_expired_pubsub_items, tags = [purge],
+ desc = "Delete expired PubSub items",
+ note = "added in 21.12",
+ module = ?MODULE, function = delete_expired_items,
+ args = [],
+ result = {res, rescode},
+ result_desc = "0 if command failed, 1 when succeeded",
result_example = ok}].
-spec mod_opt_type(atom()) -> econf:validator().
@@ -4204,6 +4270,8 @@ mod_opt_type(last_item_cache) ->
econf:bool();
mod_opt_type(max_items_node) ->
econf:non_neg_int(unlimited);
+mod_opt_type(max_item_expire_node) ->
+ econf:timeout(second, infinity);
mod_opt_type(max_nodes_discoitems) ->
econf:non_neg_int(infinity);
mod_opt_type(max_subscriptions_node) ->
@@ -4251,6 +4319,7 @@ mod_options(Host) ->
{ignore_pep_from_offline, true},
{last_item_cache, false},
{max_items_node, ?MAXITEMS},
+ {max_item_expire_node, infinity},
{max_nodes_discoitems, 100},
{nodetree, ?STDTREE},
{pep_mapping, []},
@@ -4329,11 +4398,17 @@ mod_doc() ->
" so many nodes, caching last items speeds up pubsub "
"and allows to raise user connection rate. The cost "
"is memory usage, as every item is stored in memory.")}},
+ {max_item_expire_node,
+ #{value => "timeout() | infinity",
+ note => "added in 21.12",
+ desc =>
+ ?T("Specify the maximum item epiry time. Default value "
+ "is: 'infinity'.")}},
{max_items_node,
#{value => "non_neg_integer() | infinity",
desc =>
?T("Define the maximum number of items that can be "
- "stored in a node. Default value is: '10'.")}},
+ "stored in a node. Default value is: '1000'.")}},
{max_nodes_discoitems,
#{value => "pos_integer() | infinity",
desc =>
diff --git a/src/mod_pubsub_opt.erl b/src/mod_pubsub_opt.erl
index 8db5532f6..cb3c014b9 100644
--- a/src/mod_pubsub_opt.erl
+++ b/src/mod_pubsub_opt.erl
@@ -11,6 +11,7 @@
-export([hosts/1]).
-export([ignore_pep_from_offline/1]).
-export([last_item_cache/1]).
+-export([max_item_expire_node/1]).
-export([max_items_node/1]).
-export([max_nodes_discoitems/1]).
-export([max_subscriptions_node/1]).
@@ -68,7 +69,13 @@ last_item_cache(Opts) when is_map(Opts) ->
last_item_cache(Host) ->
gen_mod:get_module_opt(Host, mod_pubsub, last_item_cache).
--spec max_items_node(gen_mod:opts() | global | binary()) -> non_neg_integer().
+-spec max_item_expire_node(gen_mod:opts() | global | binary()) -> 'infinity' | pos_integer().
+max_item_expire_node(Opts) when is_map(Opts) ->
+ gen_mod:get_opt(max_item_expire_node, Opts);
+max_item_expire_node(Host) ->
+ gen_mod:get_module_opt(Host, mod_pubsub, max_item_expire_node).
+
+-spec max_items_node(gen_mod:opts() | global | binary()) -> 'unlimited' | non_neg_integer().
max_items_node(Opts) when is_map(Opts) ->
gen_mod:get_opt(max_items_node, Opts);
max_items_node(Host) ->
diff --git a/src/mod_push_sql.erl b/src/mod_push_sql.erl
index f89b904c2..c024a12d1 100644
--- a/src/mod_push_sql.erl
+++ b/src/mod_push_sql.erl
@@ -52,7 +52,7 @@ store_session(LUser, LServer, NowTS, PushJID, Node, XData) ->
case ?SQL_UPSERT(LServer, "push_session",
["!username=%(LUser)s",
"!server_host=%(LServer)s",
- "!timestamp=%(TS)d",
+ "timestamp=%(TS)d",
"!service=%(Service)s",
"!node=%(Node)s",
"xml=%(XML)s"]) of
diff --git a/src/mod_register.erl b/src/mod_register.erl
index 379318da6..b85efd57c 100644
--- a/src/mod_register.erl
+++ b/src/mod_register.erl
@@ -32,11 +32,13 @@
-behaviour(gen_mod).
-export([start/2, stop/1, reload/3, stream_feature_register/2,
- c2s_unauthenticated_packet/2, try_register/4,
+ c2s_unauthenticated_packet/2, try_register/4, try_register/5,
process_iq/1, send_registration_notifications/3,
mod_opt_type/1, mod_options/1, depends/2,
format_error/1, mod_doc/0]).
+-deprecated({try_register, 4}).
+
-include("logger.hrl").
-include_lib("xmpp/include/xmpp.hrl").
-include("translate.hrl").
@@ -283,7 +285,7 @@ try_register_or_set_password(User, Server, Password,
_ when CaptchaSucceed ->
case check_from(From, Server) of
allow ->
- case try_register(User, Server, Password, Source, Lang) of
+ case try_register(User, Server, Password, Source, ?MODULE, Lang) of
ok ->
xmpp:make_iq_result(IQ);
{error, Error} ->
@@ -328,6 +330,13 @@ try_set_password(User, Server, Password, #iq{lang = Lang, meta = M} = IQ) ->
xmpp:make_error(IQ, xmpp:err_internal_server_error(format_error(Why), Lang))
end.
+try_register(User, Server, Password, SourceRaw, Module) ->
+ Modules = mod_register_opt:allow_modules(Server),
+ case (Modules == all) orelse lists:member(Module, Modules) of
+ true -> try_register(User, Server, Password, SourceRaw);
+ false -> {error, eaccess}
+ end.
+
try_register(User, Server, Password, SourceRaw) ->
case jid:is_nodename(User) of
false ->
@@ -363,8 +372,8 @@ try_register(User, Server, Password, SourceRaw) ->
end
end.
-try_register(User, Server, Password, SourceRaw, Lang) ->
- case try_register(User, Server, Password, SourceRaw) of
+try_register(User, Server, Password, SourceRaw, Module, Lang) ->
+ case try_register(User, Server, Password, SourceRaw, Module) of
ok ->
JID = jid:make(User, Server),
Source = may_remove_resource(SourceRaw),
@@ -597,6 +606,8 @@ mod_opt_type(access_from) ->
econf:acl();
mod_opt_type(access_remove) ->
econf:acl();
+mod_opt_type(allow_modules) ->
+ econf:either(all, econf:list(econf:atom()));
mod_opt_type(captcha_protected) ->
econf:bool();
mod_opt_type(ip_access) ->
@@ -623,6 +634,7 @@ mod_options(_Host) ->
[{access, all},
{access_from, none},
{access_remove, all},
+ {allow_modules, all},
{captcha_protected, false},
{ip_access, all},
{password_strength, 0},
@@ -661,6 +673,13 @@ mod_doc() ->
desc =>
?T("Specify rules to restrict access for user unregistration. "
"By default any user is able to unregister their account.")}},
+ {allow_modules,
+ #{value => "all | [Module, ...]",
+ note => "added in 21.12",
+ desc =>
+ ?T("List of modules that can register accounts, or 'all'. "
+ "The default value is 'all', which is equivalent to "
+ "something like '[mod_register, mod_register_web]'.")}},
{captcha_protected,
#{value => "true | false",
desc =>
diff --git a/src/mod_register_opt.erl b/src/mod_register_opt.erl
index 53c6ca6ea..e7236424c 100644
--- a/src/mod_register_opt.erl
+++ b/src/mod_register_opt.erl
@@ -6,6 +6,7 @@
-export([access/1]).
-export([access_from/1]).
-export([access_remove/1]).
+-export([allow_modules/1]).
-export([captcha_protected/1]).
-export([ip_access/1]).
-export([password_strength/1]).
@@ -31,6 +32,12 @@ access_remove(Opts) when is_map(Opts) ->
access_remove(Host) ->
gen_mod:get_module_opt(Host, mod_register, access_remove).
+-spec allow_modules(gen_mod:opts() | global | binary()) -> 'all' | [atom()].
+allow_modules(Opts) when is_map(Opts) ->
+ gen_mod:get_opt(allow_modules, Opts);
+allow_modules(Host) ->
+ gen_mod:get_module_opt(Host, mod_register, allow_modules).
+
-spec captcha_protected(gen_mod:opts() | global | binary()) -> boolean().
captcha_protected(Opts) when is_map(Opts) ->
gen_mod:get_opt(captcha_protected, Opts);
diff --git a/src/mod_register_web.erl b/src/mod_register_web.erl
index 0e216c81c..0cf4bcff8 100644
--- a/src/mod_register_web.erl
+++ b/src/mod_register_web.erl
@@ -85,7 +85,7 @@ process([Section],
process([<<"new">>],
#request{method = 'POST', q = Q, ip = {Ip, _Port},
lang = Lang, host = _HTTPHost}) ->
- case form_new_post(Q) of
+ case form_new_post(Q, Ip) of
{success, ok, {Username, Host, _Password}} ->
Jid = jid:make(Username, Host),
mod_register:send_registration_notifications(?MODULE, Jid, Ip),
@@ -290,10 +290,10 @@ form_new_get2(Host, Lang, CaptchaEls) ->
%%% Formulary new POST
%%%----------------------------------------------------------------------
-form_new_post(Q) ->
+form_new_post(Q, Ip) ->
case catch get_register_parameters(Q) of
[Username, Host, Password, Password, Id, Key] ->
- form_new_post(Username, Host, Password, {Id, Key});
+ form_new_post(Username, Host, Password, {Id, Key}, Ip);
[_Username, _Host, _Password, _Password2, false, false] ->
{error, passwords_not_identical};
[_Username, _Host, _Password, _Password2, Id, Key] ->
@@ -312,13 +312,12 @@ get_register_parameters(Q) ->
[<<"username">>, <<"host">>, <<"password">>, <<"password2">>,
<<"id">>, <<"key">>]).
-form_new_post(Username, Host, Password,
- {false, false}) ->
- register_account(Username, Host, Password);
-form_new_post(Username, Host, Password, {Id, Key}) ->
+form_new_post(Username, Host, Password, {false, false}, Ip) ->
+ register_account(Username, Host, Password, Ip);
+form_new_post(Username, Host, Password, {Id, Key}, Ip) ->
case ejabberd_captcha:check_captcha(Id, Key) of
captcha_valid ->
- register_account(Username, Host, Password);
+ register_account(Username, Host, Password, Ip);
captcha_non_valid -> {error, captcha_non_valid};
captcha_not_found -> {error, captcha_non_valid}
end.
@@ -502,11 +501,11 @@ form_del_get(Host, Lang) ->
{<<"Content-Type">>, <<"text/html">>}],
ejabberd_web:make_xhtml(HeadEls, Els)}.
-%% @spec(Username, Host, Password) -> {success, ok, {Username, Host, Password} |
+%% @spec(Username, Host, Password, Ip) -> {success, ok, {Username, Host, Password} |
%% {success, exists, {Username, Host, Password}} |
%% {error, not_allowed} |
%% {error, invalid_jid}
-register_account(Username, Host, Password) ->
+register_account(Username, Host, Password, Ip) ->
try mod_register_opt:access(Host) of
Access ->
case jid:make(Username, Host) of
@@ -514,16 +513,15 @@ register_account(Username, Host, Password) ->
JID ->
case acl:match_rule(Host, Access, JID) of
deny -> {error, not_allowed};
- allow -> register_account2(Username, Host, Password)
+ allow -> register_account2(Username, Host, Password, Ip)
end
end
catch _:{module_not_loaded, mod_register, _Host} ->
{error, host_unknown}
end.
-register_account2(Username, Host, Password) ->
- case ejabberd_auth:try_register(Username, Host,
- Password)
+register_account2(Username, Host, Password, Ip) ->
+ case mod_register:try_register(Username, Host, Password, Ip, ?MODULE)
of
ok ->
{success, ok, {Username, Host, Password}};
@@ -579,12 +577,8 @@ get_error_text({error, exists}) ->
?T("The account already exists");
get_error_text({error, password_incorrect}) ->
?T("Incorrect password");
-get_error_text({error, invalid_jid}) ->
- ?T("The username is not valid");
get_error_text({error, host_unknown}) ->
?T("Host unknown");
-get_error_text({error, not_allowed}) ->
- ?T("Not allowed");
get_error_text({error, account_doesnt_exist}) ->
?T("Account doesn't exist");
get_error_text({error, account_exists}) ->
@@ -594,7 +588,9 @@ get_error_text({error, password_not_changed}) ->
get_error_text({error, passwords_not_identical}) ->
?T("The passwords are different");
get_error_text({error, wrong_parameters}) ->
- ?T("Wrong parameters in the web formulary").
+ ?T("Wrong parameters in the web formulary");
+get_error_text({error, Why}) ->
+ mod_register:format_error(Why).
mod_options(_) ->
[].
diff --git a/src/mod_roster_sql.erl b/src/mod_roster_sql.erl
index 76ddb29dd..ebfcde463 100644
--- a/src/mod_roster_sql.erl
+++ b/src/mod_roster_sql.erl
@@ -80,9 +80,10 @@ get_roster(LUser, LServer) ->
[]
end,
GroupsDict = lists:foldl(fun({J, G}, Acc) ->
- dict:append(J, G, Acc)
+ Gs = maps:get(J, Acc, []),
+ maps:put(J, [G | Gs], Acc)
end,
- dict:new(), JIDGroups),
+ maps:new(), JIDGroups),
{ok, lists:flatmap(
fun(I) ->
case raw_to_record(LServer, I) of
@@ -90,10 +91,7 @@ get_roster(LUser, LServer) ->
error -> [];
R ->
SJID = jid:encode(R#roster.jid),
- Groups = case dict:find(SJID, GroupsDict) of
- {ok, Gs} -> Gs;
- error -> []
- end,
+ Groups = maps:get(SJID, GroupsDict, []),
[R#roster{groups = Groups}]
end
end, Items)};
diff --git a/src/mod_shared_roster.erl b/src/mod_shared_roster.erl
index 13ff90466..358a8df32 100644
--- a/src/mod_shared_roster.erl
+++ b/src/mod_shared_roster.erl
@@ -870,12 +870,15 @@ c2s_self_presence(Acc) ->
Acc.
-spec unset_presence(binary(), binary(), binary(), binary()) -> ok.
-unset_presence(LUser, LServer, Resource, Status) ->
+unset_presence(User, Server, Resource, Status) ->
+ LUser = jid:nodeprep(User),
+ LServer = jid:nameprep(Server),
+ LResource = jid:resourceprep(Resource),
Resources = ejabberd_sm:get_user_resources(LUser,
LServer),
?DEBUG("Unset_presence for ~p @ ~p / ~p -> ~p "
"(~p resources)",
- [LUser, LServer, Resource, Status, length(Resources)]),
+ [LUser, LServer, LResource, Status, length(Resources)]),
case length(Resources) of
0 ->
lists:foreach(
diff --git a/src/mod_shared_roster_ldap.erl b/src/mod_shared_roster_ldap.erl
index 08fbe8793..e842ab261 100644
--- a/src/mod_shared_roster_ldap.erl
+++ b/src/mod_shared_roster_ldap.erl
@@ -689,9 +689,9 @@ mod_doc() ->
?T("- Connection parameters: The module also accepts the "
"connection parameters, all of which default to the top-level "
"parameter of the same name, if unspecified. "
- "See http://../database-ldap/#ldap-connection[LDAP Connection] "
+ "See http://../ldap/#ldap-connection[LDAP Connection] "
"section for more information about them."), "",
- ?T("Check also the http://../database-ldap/#configuration-examples"
+ ?T("Check also the http://../ldap/#ldap-examples"
"[Configuration examples] section to get details about "
"retrieving the roster, "
"and configuration examples including Flat DIT and Deep DIT.")],
@@ -721,13 +721,13 @@ mod_doc() ->
"name of roster entries (usually full names of people in "
"the roster). See also the parameters 'ldap_userdesc' and "
"'ldap_useruid'. For more information check the LDAP "
- "http://../database-ldap/#filters[Filters] section.")}},
+ "http://../ldap/#filters[Filters] section.")}},
{ldap_filter,
#{desc =>
?T("Additional filter which is AND-ed together "
"with \"User Filter\" and \"Group Filter\". "
"For more information check the LDAP "
- "http://../database-ldap/#filters[Filters] section.")}},
+ "http://../ldap/#filters[Filters] section.")}},
%% Attributes:
{ldap_groupattr,
#{desc =>
@@ -785,7 +785,7 @@ mod_doc() ->
#{desc =>
?T("A regex for extracting user ID from the value of the "
"attribute named by 'ldap_memberattr'. Check the LDAP "
- "http://../database-ldap/#control-parameters"
+ "http://../ldap/#control-parameters"
"[Control Parameters] section.")}},
{ldap_auth_check,
#{value => "true | false",
diff --git a/src/mod_stun_disco.erl b/src/mod_stun_disco.erl
index 6e7592453..cbb671639 100644
--- a/src/mod_stun_disco.erl
+++ b/src/mod_stun_disco.erl
@@ -646,7 +646,7 @@ get_listener_ips(#{ip := {0, 0, 0, 0, 0, 0, 0, 1}} = Opts) ->
{undefined, get_turn_ipv6_addr(Opts)};
get_listener_ips(#{ip := {_, _, _, _} = IP}) ->
{IP, undefined};
-get_listener_ips(#{ip := {_, _, _, _, _,_, _, _, _} = IP}) ->
+get_listener_ips(#{ip := {_, _, _, _, _, _, _, _} = IP}) ->
{undefined, IP}.
-spec get_turn_ipv4_addr(map()) -> inet:ip4_address() | undefined.
diff --git a/src/node_flat.erl b/src/node_flat.erl
index c597b9ce9..55dea0d8d 100644
--- a/src/node_flat.erl
+++ b/src/node_flat.erl
@@ -40,7 +40,7 @@
create_node_permission/6, create_node/2, delete_node/1,
purge_node/2, subscribe_node/8, unsubscribe_node/4,
publish_item/7, delete_item/4,
- remove_extra_items/2, remove_extra_items/3,
+ remove_extra_items/2, remove_extra_items/3, remove_expired_items/2,
get_entity_affiliations/2, get_node_affiliations/1,
get_affiliation/2, set_affiliation/3,
get_entity_subscriptions/2, get_node_subscriptions/1,
@@ -432,6 +432,22 @@ remove_extra_items(Nidx, MaxItems, ItemIds) ->
del_items(Nidx, OldItems),
{result, {NewItems, OldItems}}.
+remove_expired_items(_Nidx, infinity) ->
+ {result, []};
+remove_expired_items(Nidx, Seconds) ->
+ Items = mnesia:index_read(pubsub_item, Nidx, #pubsub_item.nodeidx),
+ ExpT = misc:usec_to_now(
+ erlang:system_time(microsecond) - (Seconds * 1000000)),
+ ExpItems = lists:filtermap(
+ fun(#pubsub_item{itemid = {ItemId, _},
+ modification = {ModT, _}}) when ModT < ExpT ->
+ {true, ItemId};
+ (#pubsub_item{}) ->
+ false
+ end, Items),
+ del_items(Nidx, ExpItems),
+ {result, ExpItems}.
+
%% @doc <p>Triggers item deletion.</p>
%% <p>Default plugin: The user performing the deletion must be the node owner
%% or a publisher, or PublishModel being open.</p>
diff --git a/src/node_flat_sql.erl b/src/node_flat_sql.erl
index 240dc3760..f9c8a209d 100644
--- a/src/node_flat_sql.erl
+++ b/src/node_flat_sql.erl
@@ -43,7 +43,7 @@
create_node_permission/6, create_node/2, delete_node/1, purge_node/2,
subscribe_node/8, unsubscribe_node/4,
publish_item/7, delete_item/4,
- remove_extra_items/2, remove_extra_items/3,
+ remove_extra_items/2, remove_extra_items/3, remove_expired_items/2,
get_entity_affiliations/2, get_node_affiliations/1,
get_affiliation/2, set_affiliation/3,
get_entity_subscriptions/2, get_node_subscriptions/1,
@@ -285,6 +285,23 @@ remove_extra_items(Nidx, MaxItems, ItemIds) ->
del_items(Nidx, OldItems),
{result, {NewItems, OldItems}}.
+remove_expired_items(_Nidx, infinity) ->
+ {result, []};
+remove_expired_items(Nidx, Seconds) ->
+ ExpT = encode_now(
+ misc:usec_to_now(
+ erlang:system_time(microsecond) - (Seconds * 1000000))),
+ case ejabberd_sql:sql_query_t(
+ ?SQL("select @(itemid)s from pubsub_item where nodeid=%(Nidx)d "
+ "and creation < %(ExpT)s")) of
+ {selected, RItems} ->
+ ItemIds = [ItemId || {ItemId} <- RItems],
+ del_items(Nidx, ItemIds),
+ {result, ItemIds};
+ _ ->
+ {result, []}
+ end.
+
delete_item(Nidx, Publisher, PublishModel, ItemId) ->
SubKey = jid:tolower(Publisher),
GenKey = jid:remove_resource(SubKey),
diff --git a/src/node_pep.erl b/src/node_pep.erl
index 44388ca31..66431b948 100644
--- a/src/node_pep.erl
+++ b/src/node_pep.erl
@@ -36,7 +36,7 @@
create_node_permission/6, create_node/2, delete_node/1,
purge_node/2, subscribe_node/8, unsubscribe_node/4,
publish_item/7, delete_item/4,
- remove_extra_items/2, remove_extra_items/3,
+ remove_extra_items/2, remove_extra_items/3, remove_expired_items/2,
get_entity_affiliations/2, get_node_affiliations/1,
get_affiliation/2, set_affiliation/3,
get_entity_subscriptions/2, get_node_subscriptions/1,
@@ -81,10 +81,12 @@ features() ->
[<<"create-nodes">>,
<<"auto-create">>,
<<"auto-subscribe">>,
+ <<"config-node">>,
<<"delete-nodes">>,
<<"delete-items">>,
<<"filtered-notifications">>,
<<"modify-affiliations">>,
+ <<"multi-items">>,
<<"outcast-affiliation">>,
<<"persistent-items">>,
<<"publish">>,
@@ -142,6 +144,9 @@ remove_extra_items(Nidx, MaxItems) ->
remove_extra_items(Nidx, MaxItems, ItemIds) ->
node_flat:remove_extra_items(Nidx, MaxItems, ItemIds).
+remove_expired_items(Nidx, Seconds) ->
+ node_flat:remove_expired_items(Nidx, Seconds).
+
delete_item(Nidx, Publisher, PublishModel, ItemId) ->
node_flat:delete_item(Nidx, Publisher, PublishModel, ItemId).
diff --git a/src/node_pep_sql.erl b/src/node_pep_sql.erl
index c0cf2b166..3bb66bc4c 100644
--- a/src/node_pep_sql.erl
+++ b/src/node_pep_sql.erl
@@ -38,7 +38,7 @@
create_node_permission/6, create_node/2, delete_node/1,
purge_node/2, subscribe_node/8, unsubscribe_node/4,
publish_item/7, delete_item/4,
- remove_extra_items/2, remove_extra_items/3,
+ remove_extra_items/2, remove_extra_items/3, remove_expired_items/2,
get_entity_affiliations/2, get_node_affiliations/1,
get_affiliation/2, set_affiliation/3,
get_entity_subscriptions/2, get_node_subscriptions/1,
@@ -99,6 +99,9 @@ remove_extra_items(Nidx, MaxItems) ->
remove_extra_items(Nidx, MaxItems, ItemIds) ->
node_flat_sql:remove_extra_items(Nidx, MaxItems, ItemIds).
+remove_expired_items(Nidx, Seconds) ->
+ node_flat_sql:remove_expired_items(Nidx, Seconds).
+
delete_item(Nidx, Publisher, PublishModel, ItemId) ->
node_flat_sql:delete_item(Nidx, Publisher, PublishModel, ItemId).
diff --git a/src/prosody2ejabberd.erl b/src/prosody2ejabberd.erl
index 3992a4034..8f5c35f84 100644
--- a/src/prosody2ejabberd.erl
+++ b/src/prosody2ejabberd.erl
@@ -118,7 +118,7 @@ eval_file(Path) ->
case luerl:eval(NewData, State1) of
{ok, _} = Res ->
Res;
- {error, Why} = Err ->
+ {error, Why, _} = Err ->
?ERROR_MSG("Failed to eval ~ts: ~p", [Path, Why]),
Err
end;
diff --git a/src/rest.erl b/src/rest.erl
index d724352f2..1bb5c5ef7 100644
--- a/src/rest.erl
+++ b/src/rest.erl
@@ -191,13 +191,26 @@ base_url(Server, Path) ->
_ -> Url
end.
+-ifdef(HAVE_URI_STRING).
+uri_hack(Str) ->
+ case uri_string:normalize("%25") of
+ "%" -> % This hack around bug in httpc >21 <23.2
+ binary:replace(Str, <<"%25">>, <<"%2525">>, [global]);
+ _ -> Str
+ end.
+-else.
+uri_hack(Str) ->
+ Str.
+-endif.
+
url(Url, []) ->
Url;
url(Url, Params) ->
L = [<<"&", (iolist_to_binary(Key))/binary, "=",
(misc:url_encode(Value))/binary>>
|| {Key, Value} <- Params],
- <<$&, Encoded/binary>> = iolist_to_binary(L),
+ <<$&, Encoded0/binary>> = iolist_to_binary(L),
+ Encoded = uri_hack(Encoded0),
<<Url/binary, $?, Encoded/binary>>.
url(Server, Path, Params) ->
case binary:split(base_url(Server, Path), <<"?">>) of
diff --git a/test/roster_tests.erl b/test/roster_tests.erl
index a3b6009c9..3092b8cd8 100644
--- a/test/roster_tests.erl
+++ b/test/roster_tests.erl
@@ -224,13 +224,21 @@ get_items(Config, Version) ->
sub_els = [#roster_query{ver = Version}]}) of
#iq{type = result,
sub_els = [#roster_query{ver = NewVersion, items = Items}]} ->
- {NewVersion, Items};
+ {NewVersion, normalize_items(Items)};
#iq{type = result, sub_els = []} ->
{empty, []};
#iq{type = error} = Err ->
xmpp:get_error(Err)
end.
+normalize_items(Items) ->
+ Items2 =
+ lists:map(
+ fun(I) ->
+ I#roster_item{groups = lists:sort(I#roster_item.groups)}
+ end, Items),
+ lists:sort(Items2).
+
get_item(Config, JID) ->
case get_items(Config) of
{_Ver, Items} when is_list(Items) ->
diff --git a/tools/captcha.sh b/tools/captcha.sh
index 9fa4a52c4..7885858a2 100755
--- a/tools/captcha.sh
+++ b/tools/captcha.sh
@@ -15,69 +15,62 @@
INPUT=$1
-if test -n ${BASH_VERSION:-''} ; then
- get_random ()
- {
- R=$RANDOM
- }
-else
- for n in `od -A n -t u2 -N 48 /dev/urandom`; do RL="$RL$n "; done
- get_random ()
- {
- R=${RL%% *}
- RL=${RL#* }
- }
-fi
+for n in $(od -A n -t u2 -N 48 /dev/urandom); do RL="$RL$n "; done
+get_random ()
+{
+ R=${RL%% *}
+ RL=${RL#* }
+}
get_random
-WAVE1_AMPLITUDE=$((2 + $R % 5))
+WAVE1_AMPLITUDE=$((2 + R % 5))
get_random
-WAVE1_LENGTH=$((50 + $R % 25))
+WAVE1_LENGTH=$((50 + R % 25))
get_random
-WAVE2_AMPLITUDE=$((2 + $R % 5))
+WAVE2_AMPLITUDE=$((2 + R % 5))
get_random
-WAVE2_LENGTH=$((50 + $R % 25))
+WAVE2_LENGTH=$((50 + R % 25))
get_random
-WAVE3_AMPLITUDE=$((2 + $R % 5))
+WAVE3_AMPLITUDE=$((2 + R % 5))
get_random
-WAVE3_LENGTH=$((50 + $R % 25))
+WAVE3_LENGTH=$((50 + R % 25))
get_random
-W1_LINE_START_Y=$((10 + $R % 40))
+W1_LINE_START_Y=$((10 + R % 40))
get_random
-W1_LINE_STOP_Y=$((10 + $R % 40))
+W1_LINE_STOP_Y=$((10 + R % 40))
get_random
-W2_LINE_START_Y=$((10 + $R % 40))
+W2_LINE_START_Y=$((10 + R % 40))
get_random
-W2_LINE_STOP_Y=$((10 + $R % 40))
+W2_LINE_STOP_Y=$((10 + R % 40))
get_random
-W3_LINE_START_Y=$((10 + $R % 40))
+W3_LINE_START_Y=$((10 + R % 40))
get_random
-W3_LINE_STOP_Y=$((10 + $R % 40))
+W3_LINE_STOP_Y=$((10 + R % 40))
get_random
-B1_LINE_START_Y=$(($R % 40))
+B1_LINE_START_Y=$((R % 40))
get_random
-B1_LINE_STOP_Y=$(($R % 40))
+B1_LINE_STOP_Y=$((R % 40))
get_random
-B2_LINE_START_Y=$(($R % 40))
+B2_LINE_START_Y=$((R % 40))
get_random
-B2_LINE_STOP_Y=$(($R % 40))
-#B3_LINE_START_Y=$(($R % 40))
-#B3_LINE_STOP_Y=$(($R % 40))
+B2_LINE_STOP_Y=$((R % 40))
+#B3_LINE_START_Y=$((R % 40))
+#B3_LINE_STOP_Y=$((R % 40))
get_random
-B1_LINE_START_X=$(($R % 20))
+B1_LINE_START_X=$((R % 20))
get_random
-B1_LINE_STOP_X=$((100 + $R % 40))
+B1_LINE_STOP_X=$((100 + R % 40))
get_random
-B2_LINE_START_X=$(($R % 20))
+B2_LINE_START_X=$((R % 20))
get_random
-B2_LINE_STOP_X=$((100 + $R % 40))
-#B3_LINE_START_X=$(($R % 20))
-#B3_LINE_STOP_X=$((100 + $R % 40))
+B2_LINE_STOP_X=$((100 + R % 40))
+#B3_LINE_START_X=$((R % 20))
+#B3_LINE_STOP_X=$((100 + R % 40))
get_random
-ROLL_X=$(($R % 40))
+ROLL_X=$((R % 40))
convert -size 180x60 xc:none -pointsize 40 \
\( -clone 0 -fill white \
diff --git a/vars.config.in b/vars.config.in
index 9b3ac7585..04024fd73 100644
--- a/vars.config.in
+++ b/vars.config.in
@@ -51,9 +51,10 @@
{release, true}.
{release_dir, "${SCRIPT_DIR%/*}"}.
{sysconfdir, "{{release_dir}}/etc"}.
+{erts_dir, "{{release_dir}}/erts-${ERTS_VSN#erts-}"}.
{installuser, "@INSTALLUSER@"}.
-{erl, "{{release_dir}}/{{erts_vsn}}/bin/erl"}.
-{epmd, "{{release_dir}}/{{erts_vsn}}/bin/epmd"}.
+{erl, "{{erts_dir}}/bin/erl"}.
+{epmd, "{{erts_dir}}/bin/epmd"}.
{localstatedir, "{{release_dir}}/var"}.
{libdir, "{{release_dir}}/lib"}.
{docdir, "{{release_dir}}/doc"}.